Allow copy and paste on logins

- allow shell open for linking - some platforms it's not working as expected
This commit is contained in:
Tommy Verrall
2025-04-09 14:55:12 +02:00
parent 4cd0f7b56f
commit eb612d47c0
6 changed files with 253 additions and 7 deletions
@@ -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"]}}
@@ -0,0 +1,117 @@
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) {
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) {
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,123 @@
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) {
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) {
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>
);
};
@@ -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
/>