Merge pull request #5699 from nymtech/fix/sign-in-page-wallet
Allow copy and paste on logins fields for the wallet
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
"updater:allow-download",
|
||||
"updater:allow-download-and-install",
|
||||
"updater:allow-install",
|
||||
"core:event:allow-listen"
|
||||
"core:event:allow-listen",
|
||||
"shell:allow-open"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"main-capability":{"identifier":"main-capability","description":"Default capability for Nym Wallet main window","local":true,"windows":["main","nymWalletApp","log"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","opener:allow-open-url","opener:allow-default-urls","core:window:allow-set-title","core:app:allow-version","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","updater:default","updater:allow-check","updater:allow-download","updater:allow-download-and-install","updater:allow-install","core:event:allow-listen"],"platforms":["linux","macOS","windows"]}}
|
||||
{"main-capability":{"identifier":"main-capability","description":"Default capability for Nym Wallet main window","local":true,"windows":["main","nymWalletApp","log"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","opener:allow-open-url","opener:allow-default-urls","core:window:allow-set-title","core:app:allow-version","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","updater:default","updater:allow-check","updater:allow-download","updater:allow-download-and-install","updater:allow-install","core:event:allow-listen","shell:allow-open"],"platforms":["linux","macOS","windows"]}}
|
||||
@@ -57,6 +57,7 @@ const FormContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [signature, setSignature] = useState('');
|
||||
|
||||
const onError = (e: string) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export const SignMessageModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const sig = await signMessage(message);
|
||||
setSignature(sig || '');
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Signing failed:', err);
|
||||
setSignature('');
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ export const CurrencyFormFieldWithPaste = ({
|
||||
processPastedText(clipboardText);
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error accessing clipboard:', err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { Box, Paper, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
|
||||
// see https://github.com/tauri-apps/tauri-plugin-log/blob/dev/webview-src/index.ts#L4
|
||||
enum LogLevel {
|
||||
@@ -29,24 +40,75 @@ const getLogLevelName = (value: LogLevel) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getLogLevelColor = (level: LogLevel, theme: any) => {
|
||||
switch (level) {
|
||||
case LogLevel.Trace:
|
||||
return {
|
||||
bg: '#e8f4f8',
|
||||
color: '#2c3e50',
|
||||
chipBg: '#e8f4f8',
|
||||
};
|
||||
case LogLevel.Debug:
|
||||
return {
|
||||
bg: '#e8f0f8',
|
||||
color: '#2c3e50',
|
||||
chipBg: '#e8f0f8',
|
||||
};
|
||||
case LogLevel.Info:
|
||||
return {
|
||||
bg: '#e8f8e8',
|
||||
color: '#2c3e50',
|
||||
chipBg: '#e8f8e8',
|
||||
};
|
||||
case LogLevel.Warn:
|
||||
return {
|
||||
bg: '#fff8e0',
|
||||
color: '#5d4037',
|
||||
chipBg: '#fff8e0',
|
||||
};
|
||||
case LogLevel.Error:
|
||||
return {
|
||||
bg: '#ffe8e8',
|
||||
color: '#c0392b',
|
||||
chipBg: '#ffe8e8',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
bg: theme.palette.mode === 'dark' ? '#333' : '#f0f0f0',
|
||||
color: theme.palette.mode === 'dark' ? '#fff' : '#000',
|
||||
chipBg: theme.palette.mode === 'dark' ? '#444' : '#e0e0e0',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// see https://github.com/tauri-apps/tauri-plugin-log/blob/dev/webview-src/index.ts#L147
|
||||
interface RecordPayload {
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
timestamp?: number; // Adding timestamp for unique key generation
|
||||
}
|
||||
|
||||
export const LogViewer: FC = () => {
|
||||
const theme = useTheme();
|
||||
const unlisten = useRef<UnlistenFn>();
|
||||
const messages = useRef<RecordPayload[]>([]);
|
||||
const [messages, setMessages] = useState<RecordPayload[]>([]);
|
||||
const [messageCount, setMessageCount] = useState(0);
|
||||
const tableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
listen('log://log', (event) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(event.payload);
|
||||
const payload = event.payload as RecordPayload;
|
||||
messages.current.unshift(payload);
|
||||
const payloadWithTimestamp = {
|
||||
...payload,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [payloadWithTimestamp, ...prev]);
|
||||
setMessageCount((prev) => prev + 1);
|
||||
|
||||
if (tableRef.current) {
|
||||
tableRef.current.scrollTop = 0;
|
||||
}
|
||||
}).then((fn) => {
|
||||
unlisten.current = fn;
|
||||
});
|
||||
@@ -59,25 +121,102 @@ export const LogViewer: FC = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100vh', width: '100vw', display: 'grid', gridTemplateRows: '1fr auto' }}>
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'grid',
|
||||
gridTemplateRows: '1fr auto',
|
||||
bgcolor: theme.palette.mode === 'dark' ? '#1e1e1e' : '#f5f5f5',
|
||||
color: theme.palette.mode === 'dark' ? '#ffffff' : '#000000',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ overflowY: 'hidden', p: 2 }}>
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '100%' }}>
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{
|
||||
maxHeight: '100%',
|
||||
bgcolor: '#ffffff',
|
||||
boxShadow: theme.shadows[3],
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
ref={tableRef}
|
||||
>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Severity</TableCell>
|
||||
<TableCell>Log message</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
bgcolor: '#f0f0f0',
|
||||
color: '#333333',
|
||||
fontWeight: 'bold',
|
||||
width: '120px',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
}}
|
||||
>
|
||||
Severity
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
bgcolor: '#f0f0f0',
|
||||
color: '#333333',
|
||||
fontWeight: 'bold',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
}}
|
||||
>
|
||||
Log message
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{messages.current.map((m) => (
|
||||
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell sx={{ padding: 1 }}>
|
||||
<Chip label={getLogLevelName(m.level)} variant="outlined" size="small" />
|
||||
</TableCell>
|
||||
<TableCell sx={{ padding: 1, fontFamily: 'Monospace' }}>{m.message}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{messages.map((m, index) => {
|
||||
const levelColors = getLogLevelColor(m.level, theme);
|
||||
return (
|
||||
<TableRow
|
||||
key={`log-${m.timestamp || index}`}
|
||||
sx={{
|
||||
bgcolor: levelColors.bg,
|
||||
'&:hover': {
|
||||
filter: 'brightness(0.95)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableCell
|
||||
sx={{
|
||||
padding: 1,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
width: '120px',
|
||||
bgcolor: 'transparent',
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={getLogLevelName(m.level)}
|
||||
variant="filled"
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: levelColors.chipBg,
|
||||
color: levelColors.color,
|
||||
fontWeight: 'medium',
|
||||
minWidth: '70px',
|
||||
border: '1px solid rgba(0,0,0,0.1)',
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
padding: 1,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.875rem',
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
color: levelColors.color,
|
||||
bgcolor: 'transparent',
|
||||
}}
|
||||
>
|
||||
{m.message}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -86,9 +225,10 @@ export const LogViewer: FC = () => {
|
||||
sx={{
|
||||
p: 1,
|
||||
textAlign: 'right',
|
||||
fontSize: 'small',
|
||||
borderTop: '2px solid',
|
||||
borderTopColor: (theme) => theme.palette.divider,
|
||||
fontSize: '0.75rem',
|
||||
borderTop: '1px solid #e0e0e0',
|
||||
bgcolor: '#ffffff',
|
||||
color: '#666666',
|
||||
}}
|
||||
>
|
||||
{messageCount} log entries since opening this window
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { PasswordInput } from '@nymproject/react/textfields/Password';
|
||||
import { readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
interface PasteButtonProps {
|
||||
onPaste: () => void;
|
||||
}
|
||||
|
||||
const PasteButton: React.FC<PasteButtonProps> = ({ onPaste }) => (
|
||||
<Tooltip title="Paste from clipboard">
|
||||
<IconButton size="small" onClick={onPaste} aria-label="paste from clipboard">
|
||||
<ContentPasteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
interface EnhancedPasswordInputProps {
|
||||
password: string;
|
||||
onUpdatePassword: (password: string) => void;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
error?: string;
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const EnhancedPasswordInput: React.FC<EnhancedPasswordInputProps> = ({
|
||||
password,
|
||||
onUpdatePassword,
|
||||
...otherProps
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const findInputElement = () => {
|
||||
if (!inputRef.current) return undefined;
|
||||
|
||||
const input = inputRef.current.querySelector('input');
|
||||
if (!input) return undefined;
|
||||
|
||||
const handleKeyDown = async (e: KeyboardEvent) => {
|
||||
if (document.activeElement !== input) return;
|
||||
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
|
||||
e.preventDefault();
|
||||
setTimeout(() => {
|
||||
(input as HTMLInputElement).select();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'v') {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const clipboardText = await readText();
|
||||
if (clipboardText) {
|
||||
onUpdatePassword(clipboardText);
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to paste text:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
input.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
};
|
||||
|
||||
const cleanup = findInputElement();
|
||||
const timeoutId = setTimeout(findInputElement, 100);
|
||||
|
||||
return () => {
|
||||
if (cleanup) cleanup();
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [onUpdatePassword]);
|
||||
|
||||
const handlePaste = async () => {
|
||||
try {
|
||||
const clipboardText = await readText();
|
||||
if (clipboardText) {
|
||||
onUpdatePassword(clipboardText);
|
||||
|
||||
const input = inputRef.current?.querySelector('input');
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to paste from clipboard:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position="relative" ref={inputRef}>
|
||||
<PasswordInput password={password} onUpdatePassword={onUpdatePassword} {...otherProps} />
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: '40px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<PasteButton onPaste={handlePaste} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { MnemonicInput as OriginalMnemonicInput } from '@nymproject/react/textfields/Mnemonic';
|
||||
import { readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
interface PasteButtonProps {
|
||||
onPaste: () => void;
|
||||
}
|
||||
|
||||
const PasteButton: React.FC<PasteButtonProps> = ({ onPaste }) => (
|
||||
<Tooltip title="Paste from clipboard">
|
||||
<IconButton size="small" onClick={onPaste} aria-label="paste from clipboard">
|
||||
<ContentPasteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
interface EnhancedMnemonicInputProps {
|
||||
mnemonic: string;
|
||||
onUpdateMnemonic: (mnemonic: string) => void;
|
||||
error?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export { OriginalMnemonicInput as MnemonicInput };
|
||||
|
||||
export const EnhancedMnemonicInput: React.FC<EnhancedMnemonicInputProps> = ({
|
||||
mnemonic,
|
||||
onUpdateMnemonic,
|
||||
...otherProps
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const findInputElement = () => {
|
||||
if (!inputRef.current) return undefined;
|
||||
|
||||
const textarea = inputRef.current.querySelector('textarea');
|
||||
const input = textarea || inputRef.current.querySelector('input');
|
||||
|
||||
if (!input) return undefined;
|
||||
|
||||
// Fix the event type issue by casting Event to KeyboardEvent
|
||||
const handleKeyDown = async (e: Event) => {
|
||||
const keyEvent = e as KeyboardEvent;
|
||||
if (document.activeElement !== input) return;
|
||||
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'a') {
|
||||
keyEvent.preventDefault();
|
||||
setTimeout(() => {
|
||||
if (textarea) {
|
||||
textarea.select();
|
||||
} else {
|
||||
(input as HTMLInputElement).select();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'v') {
|
||||
keyEvent.preventDefault();
|
||||
try {
|
||||
const clipboardText = await readText();
|
||||
if (clipboardText) {
|
||||
onUpdateMnemonic(clipboardText.trim());
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to paste text:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
input.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
};
|
||||
|
||||
const cleanup = findInputElement();
|
||||
const timeoutId = setTimeout(findInputElement, 100);
|
||||
|
||||
return () => {
|
||||
if (cleanup) cleanup();
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [onUpdateMnemonic]);
|
||||
|
||||
const handlePaste = async () => {
|
||||
try {
|
||||
const clipboardText = await readText();
|
||||
if (clipboardText) {
|
||||
onUpdateMnemonic(clipboardText.trim());
|
||||
|
||||
const textarea = inputRef.current?.querySelector('textarea');
|
||||
const input = textarea || inputRef.current?.querySelector('input');
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to paste from clipboard:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position="relative" ref={inputRef}>
|
||||
<OriginalMnemonicInput mnemonic={mnemonic} onUpdateMnemonic={onUpdateMnemonic} {...otherProps} />
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: '14px',
|
||||
top: '16px',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<PasteButton onPaste={handlePaste} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -12,7 +12,6 @@ export const TauriLink: React.FC<LinkProps & any> = (props) => {
|
||||
|
||||
if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
|
||||
event.preventDefault();
|
||||
console.log('Opening link in browser:', href);
|
||||
await openUrl(href);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Button, FormControl, Stack } from '@mui/material';
|
||||
import { AppContext } from 'src/context';
|
||||
import { isPasswordCreated } from 'src/requests';
|
||||
import { MnemonicInput } from '@nymproject/react/textfields/Mnemonic';
|
||||
import { EnhancedMnemonicInput } from 'src/components/Login/MnemonicLoginFormWrapper';
|
||||
import { Subtitle } from '../components';
|
||||
|
||||
export const SignInMnemonic = () => {
|
||||
@@ -38,7 +38,11 @@ export const SignInMnemonic = () => {
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<MnemonicInput mnemonic={mnemonic} onUpdateMnemonic={(mnc) => setMnemonic(mnc)} error={error} />
|
||||
<EnhancedMnemonicInput
|
||||
mnemonic={mnemonic}
|
||||
onUpdateMnemonic={(mnc: string) => setMnemonic(mnc)}
|
||||
error={error}
|
||||
/>
|
||||
<Button variant="contained" size="large" fullWidth type="submit">
|
||||
Sign in with mnemonic
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Button, FormControl, Stack } from '@mui/material';
|
||||
import { PasswordInput } from '@nymproject/react/textfields/Password';
|
||||
import { EnhancedPasswordInput } from 'src/components/Login/LoginPasswordFormWrapper';
|
||||
import { Subtitle } from '../components';
|
||||
import { AppContext } from '../../../context/main';
|
||||
|
||||
@@ -21,10 +21,11 @@ export const SignInPassword = () => {
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<PasswordInput
|
||||
{/* Use the password wrapper input instead */}
|
||||
<EnhancedPasswordInput
|
||||
label="Enter password"
|
||||
password={password}
|
||||
onUpdatePassword={(pswd) => setPassword(pswd)}
|
||||
onUpdatePassword={(pswd: any) => setPassword(pswd)}
|
||||
error={error}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@@ -52,6 +52,7 @@ export const NodeTestPage = () => {
|
||||
} catch (e) {
|
||||
setError('Node test failed, please try again');
|
||||
testStateRef.current = 'Stopped';
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -67,6 +68,7 @@ export const NodeTestPage = () => {
|
||||
await client.tester.init(validator, nodeTesterId);
|
||||
setNodeTestClient(client);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
setError('Failed to load node tester client, please try again');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user