diff --git a/package-lock.json b/package-lock.json index b6e9280..31150bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@hookform/resolvers": "^3.3.4", "@mui/icons-material": "^5.14.19", "@mui/material": "^5.14.20", "@nostr-dev-kit/ndk": "^2.0.5", @@ -33,6 +34,7 @@ "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.50.0", "react-redux": "^9.0.3", "react-router-dom": "^6.20.1", "react-scripts": "5.0.1", @@ -51,7 +53,8 @@ "workbox-range-requests": "^6.6.0", "workbox-routing": "^6.6.0", "workbox-strategies": "^6.6.0", - "workbox-streams": "^6.6.0" + "workbox-streams": "^6.6.0", + "yup": "^1.3.3" }, "devDependencies": { "@types/lodash.isequal": "^4.5.8", @@ -2738,6 +2741,14 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, + "node_modules/@hookform/resolvers": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz", + "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -14286,6 +14297,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14604,6 +14620,21 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-hook-form": { + "version": "7.50.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.0.tgz", + "integrity": "sha512-AOhuzM3RdP09ZCnq+Z0yvKGHK25yiOX5phwxjV9L7U6HMla10ezkBnvQ+Pk4GTuDfsC5P2zza3k8mawFwFLVuQ==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -16678,6 +16709,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -16715,6 +16751,11 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -18316,6 +18357,28 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.3.3.tgz", + "integrity": "sha512-v8QwZSsHH2K3/G9WSkp6mZKO+hugKT1EmnMqLNUcfu51HU9MDyhlETT/JgtzprnrnQHPWsjc6MUDMBp/l9fNnw==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } }, "dependencies": { @@ -20090,6 +20153,12 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, + "@hookform/resolvers": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz", + "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -28331,6 +28400,11 @@ } } }, + "property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -28585,6 +28659,12 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-hook-form": { + "version": "7.50.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.0.tgz", + "integrity": "sha512-AOhuzM3RdP09ZCnq+Z0yvKGHK25yiOX5phwxjV9L7U6HMla10ezkBnvQ+Pk4GTuDfsC5P2zza3k8mawFwFLVuQ==", + "requires": {} + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -30123,6 +30203,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -30151,6 +30236,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -31375,6 +31465,24 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "yup": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.3.3.tgz", + "integrity": "sha512-v8QwZSsHH2K3/G9WSkp6mZKO+hugKT1EmnMqLNUcfu51HU9MDyhlETT/JgtzprnrnQHPWsjc6MUDMBp/l9fNnw==", + "requires": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + }, + "dependencies": { + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" + } + } } } } diff --git a/package.json b/package.json index 6aafd75..b88283b 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@hookform/resolvers": "^3.3.4", "@mui/icons-material": "^5.14.19", "@mui/material": "^5.14.20", "@nostr-dev-kit/ndk": "^2.0.5", @@ -28,6 +29,7 @@ "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.50.0", "react-redux": "^9.0.3", "react-router-dom": "^6.20.1", "react-scripts": "5.0.1", @@ -46,7 +48,8 @@ "workbox-range-requests": "^6.6.0", "workbox-routing": "^6.6.0", "workbox-strategies": "^6.6.0", - "workbox-streams": "^6.6.0" + "workbox-streams": "^6.6.0", + "yup": "^1.3.3" }, "overrides": { "react-scripts": { diff --git a/src/components/Modal/ModalLogin/ModalLogin.tsx b/src/components/Modal/ModalLogin/ModalLogin.tsx index a10c578..bc9b838 100644 --- a/src/components/Modal/ModalLogin/ModalLogin.tsx +++ b/src/components/Modal/ModalLogin/ModalLogin.tsx @@ -1,10 +1,10 @@ +import React, { useCallback, useEffect, useState } from 'react' import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { swicCall } from '@/modules/swic' import { Modal } from '@/shared/Modal/Modal' import { MODAL_PARAMS_KEYS } from '@/types/modal' import { IconButton, Stack, Typography } from '@mui/material' -import React, { ChangeEvent, useState } from 'react' import { StyledAppLogo } from './styled' import { nip19 } from 'nostr-tools' import { Input } from '@/shared/Input/Input' @@ -12,6 +12,9 @@ import { Button } from '@/shared/Button/Button' import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined' import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined' import { useNavigate } from 'react-router-dom' +import { useForm } from 'react-hook-form' +import { FormInputType, schema } from './const' +import { yupResolver } from '@hookform/resolvers/yup' export const ModalLogin = () => { const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() @@ -22,29 +25,33 @@ export const ModalLogin = () => { const navigate = useNavigate() - const [enteredUsername, setEnteredUsername] = useState('') - const [enteredPassword, setEnteredPassword] = useState('') + const { + handleSubmit, + reset, + register, + formState: { errors }, + } = useForm({ + defaultValues: { + username: '', + password: '', + }, + resolver: yupResolver(schema), + mode: 'onSubmit', + }) + const [isPasswordShown, setIsPasswordShown] = useState(false) - const handleUsernameChange = (e: ChangeEvent) => { - setEnteredUsername(e.target.value) - } - - const handlePasswordChange = (e: ChangeEvent) => { - setEnteredPassword(e.target.value) - } - const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState) - const isFormValid = - enteredUsername.trim().length > 0 && enteredPassword.trim().length > 0 + const cleanUpStates = useCallback(() => { + setIsPasswordShown(false) + reset() + }, [reset]) - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!isFormValid) return undefined + const submitHandler = async (values: FormInputType) => { try { - const [username, domain] = enteredUsername.split('@') + const [username, domain] = values.username.split('@') const response = await fetch( `https://${domain}/.well-known/nostr.json?name=${username}`, ) @@ -56,18 +63,34 @@ export const ModalLogin = () => { const pubkey = getNpub.names[username] const npub = nip19.npubEncode(pubkey) - const passphrase = enteredPassword + const passphrase = values.password + console.log('fetch', npub, passphrase) const k: any = await swicCall('fetchKey', npub, passphrase) notify(`Fetched ${k.npub}`, 'success') + cleanUpStates() navigate(`/key/${k.npub}`) } catch (error: any) { - notify(error.message, 'error') + notify(error?.message || 'Something went wrong!', 'error') } } + + useEffect(() => { + return () => { + if (isModalOpened) { + // modal closed + cleanUpStates() + } + } + }, [isModalOpened, cleanUpStates]) + return ( - + { label='Enter a Username' fullWidth placeholder='user@nsec.app' - onChange={handleUsernameChange} - value={enteredUsername} + {...register('username')} + error={!!errors.username} /> { } type={isPasswordShown ? 'text' : 'password'} + error={!!errors.password} /> - diff --git a/src/components/Modal/ModalLogin/const.ts b/src/components/Modal/ModalLogin/const.ts new file mode 100644 index 0000000..ffba1f3 --- /dev/null +++ b/src/components/Modal/ModalLogin/const.ts @@ -0,0 +1,18 @@ +import * as yup from 'yup' + +export const schema = yup.object().shape({ + username: yup + .string() + .test('Domain validation', 'The domain is required!', function (value) { + if (!value || !value.trim().length) return false + + const USERNAME_WITH_DOMAIN_REGEXP = new RegExp( + /^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g, + ) + return USERNAME_WITH_DOMAIN_REGEXP.test(value) + }) + .required(), + password: yup.string().required().min(4), +}) + +export type FormInputType = yup.InferType diff --git a/src/components/Modal/ModalSignUp/ModalSignUp.tsx b/src/components/Modal/ModalSignUp/ModalSignUp.tsx index 1a4785b..07cfe0b 100644 --- a/src/components/Modal/ModalSignUp/ModalSignUp.tsx +++ b/src/components/Modal/ModalSignUp/ModalSignUp.tsx @@ -37,6 +37,7 @@ export const ModalSignUp = () => { ) const handleSubmit = async (e: React.FormEvent) => { + if (!enteredValue.trim().length) return e.preventDefault() try { const k: any = await swicCall('generateKey') diff --git a/src/modules/backend.ts b/src/modules/backend.ts index 32b3adb..e50ebed 100644 --- a/src/modules/backend.ts +++ b/src/modules/backend.ts @@ -286,7 +286,7 @@ export class NoauthBackend { }) if (r.status !== 200 && r.status !== 201) { console.log('Fetch error', url, method, r.status) - throw new Error('Failed to fetch' + url) + throw new Error('Failed to fetch ' + url) } return await r.json() @@ -460,6 +460,7 @@ export class NoauthBackend { } if (this.notifCallback) this.notifCallback() + this.notifCallback = null } private keyInfo(k: DbKey): KeyInfo { diff --git a/src/modules/keys.ts b/src/modules/keys.ts index afec6ac..29e3790 100644 --- a/src/modules/keys.ts +++ b/src/modules/keys.ts @@ -122,7 +122,7 @@ export class Keys { const encrypted = Buffer.concat([cipher.update(nsec), cipher.final()]) console.log("encrypted key in ", Date.now() - start) return { - enckey: `${PREFIX}:${VERSION}:${iv.toString('hex')}:${encrypted.toString('hex')}}`, + enckey: `${PREFIX}:${VERSION}:${iv.toString('hex')}:${encrypted.toString('hex')}`, pwh } } diff --git a/src/pages/AppPage/App.Page.tsx b/src/pages/AppPage/App.Page.tsx index d6c0153..f4f33d1 100644 --- a/src/pages/AppPage/App.Page.tsx +++ b/src/pages/AppPage/App.Page.tsx @@ -55,7 +55,7 @@ const AppPage = () => { try { await swicCall('deleteApp', appNpub) notify(`App: «${appName}» successfully deleted!`, 'success') - navigate(`key/${npub}`) + navigate(`/key/${npub}`) } catch (error: any) { notify(error?.message || 'Failed to delete app', 'error') } diff --git a/src/pages/KeyPage/components/styled.tsx b/src/pages/KeyPage/components/styled.tsx index 3086ab2..9652397 100644 --- a/src/pages/KeyPage/components/styled.tsx +++ b/src/pages/KeyPage/components/styled.tsx @@ -1,18 +1,22 @@ import { Input, InputProps } from '@/shared/Input/Input' import { Stack, StackProps, styled } from '@mui/material' +import { forwardRef } from 'react' -export const StyledInput = styled(({ className, ...props }: InputProps) => { - return ( - - ) -})(({ theme }) => ({ +export const StyledInput = styled( + forwardRef(({ className, ...props }, ref) => { + return ( + + ) + }), +)(({ theme }) => ({ '& > .input': { border: 'none', background: theme.palette.secondary.main, diff --git a/src/pages/KeyPage/styled.tsx b/src/pages/KeyPage/styled.tsx index 7aabec5..dc44f2c 100644 --- a/src/pages/KeyPage/styled.tsx +++ b/src/pages/KeyPage/styled.tsx @@ -1,5 +1,6 @@ import { Input, InputProps } from '@/shared/Input/Input' import { Box, Button, ButtonProps, styled, Badge } from '@mui/material' +import { forwardRef } from 'react' type StyledIconButtonProps = ButtonProps & { bgcolor_variant?: 'primary' | 'secondary' @@ -57,18 +58,21 @@ export const StyledEmptyAppsBox = styled(Box)(({ theme }) => { } }) -export const StyledInput = styled(({ className, ...props }: InputProps) => { - return ( - - ) -})(({ theme }) => ({ +export const StyledInput = styled( + forwardRef(({ className, ...props }, ref) => { + return ( + + ) + }), +)(({ theme }) => ({ '& > .input': { border: 'none', background: theme.palette.secondary.main, diff --git a/src/shared/Input/Input.tsx b/src/shared/Input/Input.tsx index 87ea87f..6593672 100644 --- a/src/shared/Input/Input.tsx +++ b/src/shared/Input/Input.tsx @@ -1,4 +1,4 @@ -import { FC, ReactNode } from 'react' +import { ReactNode, forwardRef } from 'react' import { Box, BoxProps, @@ -17,29 +17,33 @@ export type InputProps = InputBaseProps & { label?: string } -export const Input: FC = ({ - helperText, - containerProps, - helperTextProps, - label, - ...props -}) => { - return ( - - {label ? ( - - {label} - - ) : null} - - {helperText ? ( - - {helperText} - - ) : null} - - ) -} +export const Input = forwardRef( + ({ helperText, containerProps, helperTextProps, label, ...props }, ref) => { + return ( + + {label ? ( + + {label} + + ) : null} + + {helperText ? ( + + {helperText} + + ) : null} + + ) + }, +) const StyledInputContainer = styled((props: BoxProps) => )( ({ theme }) => { @@ -56,6 +60,9 @@ const StyledInputContainer = styled((props: BoxProps) => )( '& input::placeholder': { color: '#fff', }, + '&.error': { + border: '0.3px solid ' + theme.palette.error.main, + }, }, '& > .helper_text': { margin: '0.5rem 1rem 0',