From 9d565ddbde88f8624ea395a4642b18842015e5f3 Mon Sep 17 00:00:00 2001 From: Bekbolsun Date: Tue, 6 Feb 2024 22:47:40 +0600 Subject: [PATCH 1/5] save --- .../ModalConfirmConnect.tsx | 33 ++-- .../ModalConfirmEvent/ModalConfirmEvent.tsx | 17 +- src/components/Modal/ModalLogin/const.ts | 12 +- .../Modal/ModalSignUp/ModalSignUp.tsx | 25 ++- src/hooks/useProfile.ts | 40 +++++ src/layout/Header/Header.tsx | 52 +++--- .../Header/components/ListItemProfile.tsx | 31 ++++ src/layout/Header/components/ListProfiles.tsx | 34 +--- src/pages/HomePage/components/ItemKey.tsx | 12 +- src/pages/KeyPage/Key.Page.tsx | 17 +- src/pages/KeyPage/hooks/useProfile.ts | 31 ---- src/routes/AppRoutes.tsx | 1 - src/store/index.ts | 4 + src/utils/helpers/helpers.ts | 153 +++++++++--------- 14 files changed, 231 insertions(+), 231 deletions(-) create mode 100644 src/hooks/useProfile.ts create mode 100644 src/layout/Header/components/ListItemProfile.tsx delete mode 100644 src/pages/KeyPage/hooks/useProfile.ts diff --git a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx index 4377002..b3439de 100644 --- a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx +++ b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx @@ -12,7 +12,6 @@ import { useState } from 'react' import { swicCall } from '@/modules/swic' import { ACTION_TYPE } from '@/utils/consts' - export const ModalConfirmConnect = () => { const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT) @@ -45,7 +44,7 @@ export const ModalConfirmConnect = () => { sp.delete('appNpub') sp.delete('reqId') await swicCall('confirm', pendingReqId, false, false) - } + }, }, ) const closeModalAfterRequest = createHandleCloseReplace( @@ -55,27 +54,27 @@ export const ModalConfirmConnect = () => { sp.delete('appNpub') sp.delete('reqId') }, - } + }, ) async function confirmPending( id: string, allow: boolean, remember: boolean, - options?: any + options?: any, ) { call(async () => { await swicCall('confirm', id, allow, remember, options) console.log('confirmed', id, allow, remember, options) closeModalAfterRequest() }) - if (isPopup) window.close(); + if (isPopup) window.close() } const allow = () => { - const options: any = {}; + const options: any = {} if (selectedActionType === ACTION_TYPE.BASIC) - options.perms = [ACTION_TYPE.BASIC]; + options.perms = [ACTION_TYPE.BASIC] // else // options.perms = ['connect','get_public_key']; confirmPending(pendingReqId, true, true, options) @@ -86,16 +85,16 @@ export const ModalConfirmConnect = () => { } if (isPopup) { - document.addEventListener('visibilitychange', function() { - if (document.visibilityState == 'hidden') { - disallow(); + document.addEventListener('visibilitychange', function () { + if (document.visibilityState === 'hidden') { + disallow() } }) } return ( - @@ -147,16 +146,10 @@ export const ModalConfirmConnect = () => { /> - + Disallow - + {/* Allow {selectedActionType} actions */} Connect diff --git a/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx b/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx index 21550d2..8fe00ec 100644 --- a/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx +++ b/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx @@ -94,10 +94,11 @@ export const ModalConfirmEvent: FC = ({ sp.delete('appNpub') sp.delete('reqId') selectedPendingRequests.forEach( - async (req) => await swicCall('confirm', req.id, false, false), + async (req) => + await swicCall('confirm', req.id, false, false), ) - } - } + }, + }, ) const closeModalAfterRequest = createHandleCloseReplace( @@ -106,8 +107,8 @@ export const ModalConfirmEvent: FC = ({ onClose: (sp) => { sp.delete('appNpub') sp.delete('reqId') - } - } + }, + }, ) async function confirmPending(allow: boolean) { @@ -119,7 +120,7 @@ export const ModalConfirmEvent: FC = ({ }) }) closeModalAfterRequest() - if (isPopup) window.close(); + if (isPopup) window.close() } const handleChangeCheckbox = (reqId: string) => () => { @@ -141,8 +142,8 @@ export const ModalConfirmEvent: FC = ({ if (isPopup) { document.addEventListener('visibilitychange', () => { - if (document.visibilityState == 'hidden') { - confirmPending(false); + if (document.visibilityState === 'hidden') { + confirmPending(false) } }) } diff --git a/src/components/Modal/ModalLogin/const.ts b/src/components/Modal/ModalLogin/const.ts index ffba1f3..f8c6e1f 100644 --- a/src/components/Modal/ModalLogin/const.ts +++ b/src/components/Modal/ModalLogin/const.ts @@ -1,17 +1,7 @@ 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(), + username: yup.string().required(), password: yup.string().required().min(4), }) diff --git a/src/components/Modal/ModalSignUp/ModalSignUp.tsx b/src/components/Modal/ModalSignUp/ModalSignUp.tsx index 4920b23..07ff73b 100644 --- a/src/components/Modal/ModalSignUp/ModalSignUp.tsx +++ b/src/components/Modal/ModalSignUp/ModalSignUp.tsx @@ -10,7 +10,7 @@ import { Button } from '@/shared/Button/Button' import { CheckmarkIcon } from '@/assets' import { swicCall } from '@/modules/swic' import { useNavigate } from 'react-router-dom' -import { DOMAIN, NOAUTHD_URL } from '@/utils/consts' +import { DOMAIN } from '@/utils/consts' import { fetchNip05 } from '@/utils/helpers/helpers' export const ModalSignUp = () => { @@ -36,20 +36,17 @@ export const ModalSignUp = () => { } } - const inputHelperText = enteredValue - ? ( + const inputHelperText = enteredValue ? ( isAvailable ? ( <> Available ) : ( - <> - Already taken - + <>Already taken ) ) : ( "Don't worry, username can be changed later." - ); + ) const handleSubmit = async (e: React.FormEvent) => { const name = enteredValue.trim() @@ -96,13 +93,13 @@ export const ModalSignUp = () => { helperTextProps={{ sx: { '&.helper_text': { - color: enteredValue && isAvailable - ? theme.palette.success.main - : (enteredValue && !isAvailable - ? theme.palette.error.main - : theme.palette.textSecondaryDecorate.main - ) - , + color: + enteredValue && isAvailable + ? theme.palette.success.main + : enteredValue && !isAvailable + ? theme.palette.error.main + : theme.palette + .textSecondaryDecorate.main, }, }, }} diff --git a/src/hooks/useProfile.ts b/src/hooks/useProfile.ts new file mode 100644 index 0000000..c3b4554 --- /dev/null +++ b/src/hooks/useProfile.ts @@ -0,0 +1,40 @@ +import { useCallback, useEffect, useState } from 'react' +import { fetchProfile } from '@/modules/nostr' +import { MetaEvent } from '@/types/meta-event' +import { getProfileUsername, getShortenNpub } from '@/utils/helpers/helpers' +import { useAppSelector } from '@/store/hooks/redux' +import { selectKeyByNpub } from '@/store' + +const getFirstLetter = (text: string | undefined): string | null => { + if (!text || text.trim().length === 0) return null + return text.substring(0, 1).toUpperCase() +} + +export const useProfile = (npub: string) => { + const [profile, setProfile] = useState(null) + const currentKey = useAppSelector((state) => selectKeyByNpub(state, npub)) + + const userName = getProfileUsername(profile) || currentKey?.name + const userAvatar = profile?.info?.picture || '' + const avatarTitle = getFirstLetter(userName) + + const loadProfile = useCallback(async () => { + try { + const response = await fetchProfile(npub) + setProfile(response) + } catch (error) { + console.error('Failed to fetch profile:', error) + } + }, [npub]) + + useEffect(() => { + loadProfile() + }, [loadProfile]) + + return { + profile, + userName: userName || getShortenNpub(npub), + userAvatar, + avatarTitle, + } +} diff --git a/src/layout/Header/Header.tsx b/src/layout/Header/Header.tsx index 2b770c8..d43ea00 100644 --- a/src/layout/Header/Header.tsx +++ b/src/layout/Header/Header.tsx @@ -2,35 +2,20 @@ import { Avatar, Stack, Toolbar, Typography } from '@mui/material' import { AppLogo } from '../../assets' import { StyledAppBar, StyledAppName } from './styled' import { Menu } from './components/Menu' -import { useParams } from 'react-router-dom' -import { useCallback, useEffect, useState } from 'react' -import { MetaEvent } from '@/types/meta-event' -import { fetchProfile } from '@/modules/nostr' +import { useNavigate, useParams } from 'react-router-dom' import { ProfileMenu } from './components/ProfileMenu' -import { getShortenNpub } from '@/utils/helpers/helpers' +import { useProfile } from '@/hooks/useProfile' export const Header = () => { const { npub = '' } = useParams<{ npub: string }>() - const [profile, setProfile] = useState(null) + const { userName, userAvatar, avatarTitle } = useProfile(npub) + const showProfile = Boolean(npub) - const load = useCallback(async () => { - if (!npub) return setProfile(null) + const navigate = useNavigate() - try { - const response = await fetchProfile(npub) - setProfile(response as any) - } catch (e) { - return setProfile(null) - } - }, [npub]) - - useEffect(() => { - load() - }, [load]) - - const showProfile = Boolean(npub || profile) - const userName = profile?.info?.name || getShortenNpub(npub) - const userAvatar = profile?.info?.picture || '' + const handleNavigate = () => { + navigate(`/key/${npub}`) + } return ( @@ -41,17 +26,30 @@ export const Header = () => { alignItems={'center'} width={'100%'} > - {showProfile ? ( + {showProfile && ( - - {userName} + + {avatarTitle} + + + {userName} + - ) : ( + )} + + {!showProfile && ( Nsec.app diff --git a/src/layout/Header/components/ListItemProfile.tsx b/src/layout/Header/components/ListItemProfile.tsx new file mode 100644 index 0000000..f0de331 --- /dev/null +++ b/src/layout/Header/components/ListItemProfile.tsx @@ -0,0 +1,31 @@ +import { useProfile } from '@/hooks/useProfile' +import { DbKey } from '@/modules/db' +import { Avatar, ListItemIcon, MenuItem, Typography } from '@mui/material' +import React, { FC } from 'react' + +type ListItemProfileProps = { + onClickItem: () => void +} & DbKey + +export const ListItemProfile: FC = ({ + onClickItem, + npub, +}) => { + const { userName, userAvatar, avatarTitle } = useProfile(npub) + return ( + + + + {avatarTitle} + + + + {userName} + + + ) +} diff --git a/src/layout/Header/components/ListProfiles.tsx b/src/layout/Header/components/ListProfiles.tsx index fbc1c2e..70318e1 100644 --- a/src/layout/Header/components/ListProfiles.tsx +++ b/src/layout/Header/components/ListProfiles.tsx @@ -1,13 +1,7 @@ import { DbKey } from '@/modules/db' -import { getShortenNpub } from '@/utils/helpers/helpers' -import { - Avatar, - ListItemIcon, - MenuItem, - Stack, - Typography, -} from '@mui/material' -import React, { FC } from 'react' +import { Stack } from '@mui/material' +import { FC } from 'react' +import { ListItemProfile } from './ListItemProfile' type ListProfilesProps = { keys: DbKey[] @@ -21,26 +15,12 @@ export const ListProfiles: FC = ({ return ( {keys.map((key) => { - const userName = - key?.profile?.info?.name || getShortenNpub(key.npub) - const userAvatar = key?.profile?.info?.picture || '' return ( - onClickItem(key)} + - - - - - {userName} - - + onClickItem={() => onClickItem(key)} + /> ) })} diff --git a/src/pages/HomePage/components/ItemKey.tsx b/src/pages/HomePage/components/ItemKey.tsx index 9c2a236..89b2802 100644 --- a/src/pages/HomePage/components/ItemKey.tsx +++ b/src/pages/HomePage/components/ItemKey.tsx @@ -8,26 +8,26 @@ import { TypographyProps, styled, } from '@mui/material' -import { getShortenNpub } from '../../../utils/helpers/helpers' import { useNavigate } from 'react-router-dom' +import { useProfile } from '@/hooks/useProfile' type ItemKeyProps = DbKey export const ItemKey: FC = (props) => { - const { npub, profile } = props + const { npub } = props const navigate = useNavigate() + const { userName, userAvatar, avatarTitle } = useProfile(npub) const handleNavigate = () => { navigate('/key/' + npub) } - const { name = '', picture = '' } = profile?.info || {} - const userName = name || getShortenNpub(npub) - const userAvatar = picture || '' return ( - + + {avatarTitle} + {userName} diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx index 7909ccf..5f3dd4a 100644 --- a/src/pages/KeyPage/Key.Page.tsx +++ b/src/pages/KeyPage/Key.Page.tsx @@ -11,7 +11,6 @@ import { ModalSettings } from '@/components/Modal/ModalSettings/ModalSettings' import { ModalExplanation } from '@/components/Modal/ModalExplanation/ModalExplanation' import { ModalConfirmConnect } from '@/components/Modal/ModalConfirmConnect/ModalConfirmConnect' import { ModalConfirmEvent } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent' -import { useProfile } from './hooks/useProfile' import { useBackgroundSigning } from './hooks/useBackgroundSigning' import { BackgroundSigningWarning } from './components/BackgroundSigningWarning' import UserValueSection from './components/UserValueSection' @@ -22,23 +21,23 @@ import { DOMAIN } from '@/utils/consts' const KeyPage = () => { const { npub = '' } = useParams<{ npub: string }>() - const { keys, apps, pending, perms } = useAppSelector((state) => state.content) + const { keys, apps, pending, perms } = useAppSelector( + (state) => state.content, + ) const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false) const { handleOpen } = useModalSearchParams() - // const { userNameWithPrefix } = useProfile(npub) const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning() - const key = keys.find(k => k.npub === npub) + const key = keys.find((k) => k.npub === npub) + let username = '' if (key?.name) { - if (key.name.includes('@')) - username = key.name - else - username = `${key?.name}@${DOMAIN}` - } + if (key.name.includes('@')) username = key.name + else username = `${key?.name}@${DOMAIN}` + } const filteredApps = apps.filter((a) => a.npub === npub) const { prepareEventPendings } = useTriggerConfirmModal( diff --git a/src/pages/KeyPage/hooks/useProfile.ts b/src/pages/KeyPage/hooks/useProfile.ts deleted file mode 100644 index 32ff456..0000000 --- a/src/pages/KeyPage/hooks/useProfile.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' -import { fetchProfile } from '@/modules/nostr' -import { MetaEvent } from '@/types/meta-event' -import { getProfileUsername } from '@/utils/helpers/helpers' -import { DOMAIN } from '@/utils/consts' - -export const useProfile = (npub: string) => { - const [profile, setProfile] = useState(null) - - const userName = getProfileUsername(profile, npub) - // FIXME use nip05? - const userNameWithPrefix = userName + '@' + DOMAIN - - const loadProfile = useCallback(async () => { - try { - const response = await fetchProfile(npub) - setProfile(response) - } catch (error) { - console.error('Failed to fetch profile:', error) - } - }, [npub]) - - useEffect(() => { - loadProfile() - }, [loadProfile]) - - return { - profile, - userNameWithPrefix, - } -} diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index ee3b6c9..4111569 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -1,7 +1,6 @@ import { Suspense, lazy } from 'react' import { Route, Routes, Navigate } from 'react-router-dom' import HomePage from '../pages/HomePage/Home.Page' -import WelcomePage from '../pages/Welcome.Page' import { Layout } from '../layout/Layout' import { CircularProgress, Stack } from '@mui/material' diff --git a/src/store/index.ts b/src/store/index.ts index a7b2938..e942507 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -44,6 +44,10 @@ export type AppDispatch = typeof store.dispatch export const selectKeys = (state: RootState) => state.content.keys +export const selectKeyByNpub = (state: RootState, npub: string) => { + return state.content.keys.find((key) => key.npub === npub) +} + export const selectAppsByNpub = memoizeOne((state: RootState, npub: string) => { return state.content.apps.filter((app) => app.npub === npub) }, isDeepEqual) diff --git a/src/utils/helpers/helpers.ts b/src/utils/helpers/helpers.ts index bc8d343..d8d6735 100644 --- a/src/utils/helpers/helpers.ts +++ b/src/utils/helpers/helpers.ts @@ -1,101 +1,100 @@ -import { nip19 } from "nostr-tools"; -import { ACTION_TYPE, NIP46_RELAYS } from "../consts"; -import { DbPending } from "@/modules/db"; -import { MetaEvent } from "@/types/meta-event"; +import { nip19 } from 'nostr-tools' +import { ACTION_TYPE, NIP46_RELAYS } from '../consts' +import { DbPending } from '@/modules/db' +import { MetaEvent } from '@/types/meta-event' export async function call(cb: () => any) { - try { - return await cb(); - } catch (e) { - console.log(`Error: ${e}`); - } + try { + return await cb() + } catch (e) { + console.log(`Error: ${e}`) + } } -export const getShortenNpub = (npub = "") => { - return npub.substring(0, 10) + "..." + npub.slice(-4); -}; +export const getShortenNpub = (npub = '') => { + return npub.substring(0, 10) + '...' + npub.slice(-4) +} -export const getProfileUsername = (profile: MetaEvent | null, npub: string) => { - return ( - profile?.info?.name || profile?.info?.display_name || getShortenNpub(npub) - ); -}; +export const getProfileUsername = (profile: MetaEvent | null) => { + if (!profile) return null + return profile?.info?.name || profile?.info?.display_name +} -export const getBunkerLink = (npub = "") => { - if (!npub) return ""; - const { data: pubkey } = nip19.decode(npub); - return `bunker://${pubkey}?relay=${NIP46_RELAYS[0]}`; -}; +export const getBunkerLink = (npub = '') => { + if (!npub) return '' + const { data: pubkey } = nip19.decode(npub) + return `bunker://${pubkey}?relay=${NIP46_RELAYS[0]}` +} export async function askNotificationPermission() { - return new Promise((ok, rej) => { - // Let's check if the browser supports notifications - if (!("Notification" in window)) { - rej("This browser does not support notifications."); - } else { - Notification.requestPermission().then(() => { - if (Notification.permission === "granted") ok(); - else rej(); - }); - } - }); + return new Promise((ok, rej) => { + // Let's check if the browser supports notifications + if (!('Notification' in window)) { + rej('This browser does not support notifications.') + } else { + Notification.requestPermission().then(() => { + if (Notification.permission === 'granted') ok() + else rej() + }) + } + }) } export function getSignReqKind(req: DbPending): number | undefined { - try { - const data = JSON.parse(JSON.parse(req.params)[0]); - return data.kind; - } catch {} - return undefined; + try { + const data = JSON.parse(JSON.parse(req.params)[0]) + return data.kind + } catch {} + return undefined } export function getReqPerm(req: DbPending): string { - if (req.method === "sign_event") { - const kind = getSignReqKind(req); - if (kind !== undefined) return `${req.method}:${kind}`; - } - return req.method; + if (req.method === 'sign_event') { + const kind = getSignReqKind(req) + if (kind !== undefined) return `${req.method}:${kind}` + } + return req.method } export function isPackagePerm(perm: string, reqPerm: string) { - if (perm === ACTION_TYPE.BASIC) { - switch (reqPerm) { - case "connect": - case "get_public_key": - case "nip04_decrypt": - case "nip04_encrypt": - case "sign_event:0": - case "sign_event:1": - case "sign_event:3": - case "sign_event:6": - case "sign_event:7": - case "sign_event:9734": - case "sign_event:10002": - case "sign_event:30023": - case "sign_event:10000": - return true; - } - } - return false; + if (perm === ACTION_TYPE.BASIC) { + switch (reqPerm) { + case 'connect': + case 'get_public_key': + case 'nip04_decrypt': + case 'nip04_encrypt': + case 'sign_event:0': + case 'sign_event:1': + case 'sign_event:3': + case 'sign_event:6': + case 'sign_event:7': + case 'sign_event:9734': + case 'sign_event:10002': + case 'sign_event:30023': + case 'sign_event:10000': + return true + } + } + return false } export async function fetchNip05(value: string, origin?: string) { - try { - const [username, domain] = value.split("@"); + try { + const [username, domain] = value.split('@') if (!origin) origin = `https://${domain}` - const response = await fetch( - `${origin}/.well-known/nostr.json?name=${username}` - ); - const getNpub: { - names: { - [name: string]: string; - }; - } = await response.json(); + const response = await fetch( + `${origin}/.well-known/nostr.json?name=${username}`, + ) + const getNpub: { + names: { + [name: string]: string + } + } = await response.json() - const pubkey = getNpub.names[username]; - return nip19.npubEncode(pubkey); - } catch (e) { - console.log("Failed to fetch nip05", value, "error: " + e); + const pubkey = getNpub.names[username] + return nip19.npubEncode(pubkey) + } catch (e) { + console.log('Failed to fetch nip05', value, 'error: ' + e) return '' - } + } } From e4fdb7794a252c5b4b31b354b33a7519635507b8 Mon Sep 17 00:00:00 2001 From: Bekbolsun Date: Fri, 9 Feb 2024 03:42:07 +0600 Subject: [PATCH 2/5] add app details modal, refactor showing username logic, handle modals&pages in case of errors from input params, replace change theme button and etc.. --- src/App.tsx | 5 +- .../Modal/ModalAppDetails/ModalAppDetails.tsx | 173 ++++++++++++++++++ .../Modal/ModalAppDetails/styled.tsx | 11 ++ .../ModalConfirmConnect.tsx | 43 ++--- .../ModalConfirmEvent/ModalConfirmEvent.tsx | 47 ++--- .../Modal/ModalConnectApp/ModalConnectApp.tsx | 21 ++- .../Modal/ModalImportKeys/ModalImportKeys.tsx | 124 +++++++++++-- src/components/Modal/ModalImportKeys/const.ts | 8 + .../Modal/ModalLogin/ModalLogin.tsx | 45 ++--- .../Modal/ModalSettings/ModalSettings.tsx | 41 +++-- .../Modal/ModalSignUp/ModalSignUp.tsx | 42 +++-- src/hooks/usePassword.tsx | 30 +++ src/hooks/useProfile.ts | 49 ++--- src/layout/Header/Header.tsx | 29 ++- src/layout/Header/components/Menu.tsx | 19 +- src/layout/Header/components/ProfileMenu.tsx | 16 +- src/layout/Header/styled.tsx | 23 ++- src/modules/db.ts | 11 ++ src/pages/AppPage/App.Page.tsx | 40 ++-- src/pages/KeyPage/Key.Page.tsx | 21 ++- src/pages/KeyPage/components/styled.tsx | 4 +- src/pages/KeyPage/styled.tsx | 4 +- src/shared/DebounceInput/DebounceInput.tsx | 6 +- src/shared/Input/Input.tsx | 6 +- src/shared/Modal/Modal.tsx | 2 +- src/shared/Modal/styled.tsx | 6 +- src/store/index.ts | 3 +- src/types/modal.ts | 1 + src/utils/helpers/helpers.ts | 4 + 29 files changed, 598 insertions(+), 236 deletions(-) create mode 100644 src/components/Modal/ModalAppDetails/ModalAppDetails.tsx create mode 100644 src/components/Modal/ModalAppDetails/styled.tsx create mode 100644 src/components/Modal/ModalImportKeys/const.ts create mode 100644 src/hooks/usePassword.tsx diff --git a/src/App.tsx b/src/App.tsx index 6cc9609..42f3c1c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,10 +45,7 @@ function App() { const apps = await dbi.listApps() dispatch( setApps({ - apps: apps.map((app) => ({ - ...app, - //icon: 'https://nostr.band/android-chrome-192x192.png', - })), + apps, }) ) diff --git a/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx b/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx new file mode 100644 index 0000000..ca3ce23 --- /dev/null +++ b/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx @@ -0,0 +1,173 @@ +import { useModalSearchParams } from '@/hooks/useModalSearchParams' +import { Button } from '@/shared/Button/Button' +import { Input } from '@/shared/Input/Input' +import { Modal } from '@/shared/Modal/Modal' +import { MODAL_PARAMS_KEYS } from '@/types/modal' +import { Autocomplete, CircularProgress, Stack, Typography } from '@mui/material' +import { StyledInput } from './styled' +import { FormEvent, useEffect, useState } from 'react' +import { isEmptyString } from '@/utils/helpers/helpers' +import { useParams } from 'react-router-dom' +import { useAppDispatch, useAppSelector } from '@/store/hooks/redux' +import { selectApps } from '@/store' +import { dbi } from '@/modules/db' +import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' +import { setApps } from '@/store/reducers/content.slice' + +export const ModalAppDetails = () => { + const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() + const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.APP_DETAILS) + const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.APP_DETAILS) + + const { appNpub = '' } = useParams() + const apps = useAppSelector(selectApps) + const dispatch = useAppDispatch() + + const notify = useEnqueueSnackbar() + + const [details, setDetails] = useState({ + url: '', + name: '', + icon: '', + }) + const [isLoading, setIsLoading] = useState(false) + + useEffect(() => { + const currentApp = apps.find((app) => app.appNpub === appNpub) + if (!currentApp) return + + setDetails({ + icon: currentApp.icon || '', + name: currentApp.name || '', + url: currentApp.url || '', + }) + + // eslint-disable-next-line + }, [appNpub]) + + useEffect(() => { + return () => { + if (isModalOpened) { + // modal closed + setIsLoading(false) + } + } + }, [isModalOpened]) + + const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub) + + if (isModalOpened && !isAppNpubExists) { + handleCloseModal() + return null + } + + const { icon, name, url } = details + + const handleInputBlur = () => { + if (isEmptyString(url)) return + + try { + const u = new URL(url) + + if (isEmptyString(name)) setDetails((prev) => ({ ...prev, name: u.hostname })) + if (isEmptyString(icon)) { + const iconUrl = `https://${u.hostname}/favicon.ico` + setDetails((prev) => ({ ...prev, icon: iconUrl })) + } + } catch { + /* empty */ + } + } + + const handleInputChange = (key: string) => (e: React.ChangeEvent) => { + setDetails((prevState) => { + return { ...prevState, [key]: e.target.value } + }) + } + + const handleAutocompletInputChange = (e: unknown, value: string) => { + setDetails((prevState) => { + return { ...prevState, url: value } + }) + } + + const submitHandler = async (e: FormEvent) => { + e.preventDefault() + if (isLoading) return undefined + try { + setIsLoading(true) + const updatedApp = { + url, + name, + icon, + appNpub, + } + await dbi.updateApp(updatedApp) + const apps = await dbi.listApps() + dispatch( + setApps({ + apps, + }) + ) + notify(`App successfully updated!`, 'success') + setIsLoading(false) + handleCloseModal() + } catch (error: any) { + setIsLoading(false) + notify(error?.message || 'Something went wrong!', 'error') + } + } + + const isFormValid = !isEmptyString(url) && !isEmptyString(name) && !isEmptyString(icon) + + return ( + + + + + App details + + + + { + return ( + + ) + }} + /> + + + + + + + ) +} diff --git a/src/components/Modal/ModalAppDetails/styled.tsx b/src/components/Modal/ModalAppDetails/styled.tsx new file mode 100644 index 0000000..aea9c6c --- /dev/null +++ b/src/components/Modal/ModalAppDetails/styled.tsx @@ -0,0 +1,11 @@ +import { AppInputProps, Input } from '@/shared/Input/Input' +import { styled } from '@mui/material' +import { forwardRef } from 'react' + +export const StyledInput = styled( + forwardRef((props, ref) => ) +)(() => ({ + '& .MuiAutocomplete-endAdornment': { + right: '1rem', + }, +})) diff --git a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx index edaf743..bef44a8 100644 --- a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx +++ b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx @@ -5,7 +5,7 @@ import { call, getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helper import { Avatar, Box, Stack, Typography } from '@mui/material' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { useAppSelector } from '@/store/hooks/redux' -import { selectAppsByNpub } from '@/store' +import { selectAppsByNpub, selectKeys } from '@/store' import { StyledButton, StyledToggleButtonsGroup } from './styled' import { ActionToggleButton } from './сomponents/ActionToggleButton' import { useState } from 'react' @@ -14,6 +14,8 @@ import { ACTION_TYPE } from '@/utils/consts' import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' export const ModalConfirmConnect = () => { + const keys = useAppSelector(selectKeys) + const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const notify = useEnqueueSnackbar() const navigate = useNavigate() @@ -39,18 +41,6 @@ export const ModalConfirmConnect = () => { const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub) const appIcon = icon || (appDomain ? `https://${appDomain}/favicon.ico` : '') - const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => { - if (!value) return undefined - return setSelectedActionType(value) - } - - // const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, { - // onClose: async (sp) => { - // sp.delete('appNpub') - // sp.delete('reqId') - // await swicCall('confirm', pendingReqId, false, false) - // }, - // }) const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, { onClose: (sp) => { sp.delete('appNpub') @@ -61,6 +51,19 @@ export const ModalConfirmConnect = () => { }, }) + const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) + const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub) + const isPendingReqIdExists = pendingReqId.trim().length + if (isModalOpened && (!isNpubExists || !isAppNpubExists || !isPendingReqIdExists)) { + closeModalAfterRequest() + return null + } + + const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => { + if (!value) return undefined + return setSelectedActionType(value) + } + async function confirmPending(id: string, allow: boolean, remember: boolean, options?: any) { call(async () => { await swicCall('confirm', id, allow, remember, options) @@ -128,12 +131,7 @@ export const ModalConfirmConnect = () => { } return ( - + { value={ACTION_TYPE.BASIC} title="Basic permissions" description="Read your public key, sign notes, reactions, zaps, etc" - // hasinfo /> - {/* */} = ({ confirmEventReqs }) => { + const keys = useAppSelector(selectKeys) + const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT) const [searchParams] = useSearchParams() const appNpub = searchParams.get('appNpub') || '' const isPopup = searchParams.get('popup') === 'true' - const { npub = '' } = useParams<{ npub: string }>() const apps = useAppSelector((state) => selectAppsByNpub(state, npub)) @@ -53,6 +54,21 @@ export const ModalConfirmEvent: FC = ({ confirmEventReqs setPendingRequests(currentAppPendingReqs.map((pr) => ({ ...pr, checked: true }))) }, [currentAppPendingReqs]) + const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, { + onClose: (sp) => { + sp.delete('appNpub') + sp.delete('reqId') + }, + }) + + const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) + const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub) + + if (isModalOpened && (!isNpubExists || !isAppNpubExists)) { + closeModalAfterRequest() + return null + } + const triggerApp = apps.find((app) => app.appNpub === appNpub) const { name, icon = '' } = triggerApp || {} const appName = name || getShortenNpub(appNpub) @@ -65,21 +81,6 @@ export const ModalConfirmEvent: FC = ({ confirmEventReqs const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked) - const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, { - onClose: (sp) => { - sp.delete('appNpub') - sp.delete('reqId') - selectedPendingRequests.forEach(async (req) => await swicCall('confirm', req.id, false, false)) - }, - }) - - const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, { - onClose: (sp) => { - sp.delete('appNpub') - sp.delete('reqId') - }, - }) - async function confirmPending(allow: boolean) { selectedPendingRequests.forEach((req) => { call(async () => { @@ -109,12 +110,7 @@ export const ModalConfirmEvent: FC = ({ confirmEventReqs } return ( - + = ({ confirmEventReqs - {/* */} diff --git a/src/components/Modal/ModalConnectApp/ModalConnectApp.tsx b/src/components/Modal/ModalConnectApp/ModalConnectApp.tsx index 546f71d..e820134 100644 --- a/src/components/Modal/ModalConnectApp/ModalConnectApp.tsx +++ b/src/components/Modal/ModalConnectApp/ModalConnectApp.tsx @@ -5,6 +5,8 @@ import { Button } from '@/shared/Button/Button' import { Input } from '@/shared/Input/Input' import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton' import { Modal } from '@/shared/Modal/Modal' +import { selectKeys } from '@/store' +import { useAppSelector } from '@/store/hooks/redux' import { MODAL_PARAMS_KEYS } from '@/types/modal' import { getBunkerLink } from '@/utils/helpers/helpers' import { Stack, Typography } from '@mui/material' @@ -12,9 +14,14 @@ import { useRef } from 'react' import { useParams } from 'react-router-dom' export const ModalConnectApp = () => { - const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams() - const timerRef = useRef() + const keys = useAppSelector(selectKeys) + const timerRef = useRef() + const notify = useEnqueueSnackbar() + const { npub = '' } = useParams<{ npub: string }>() + const bunkerStr = getBunkerLink(npub) + + const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONNECT_APP, { onClose: () => { @@ -22,11 +29,11 @@ export const ModalConnectApp = () => { }, }) - const notify = useEnqueueSnackbar() - - const { npub = '' } = useParams<{ npub: string }>() - - const bunkerStr = getBunkerLink(npub) + const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) + if (isModalOpened && !isNpubExists) { + handleCloseModal() + return null + } const handleShareBunker = async () => { const shareData = { diff --git a/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx b/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx index 7fd2a8f..153262c 100644 --- a/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx +++ b/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx @@ -5,56 +5,142 @@ import { Button } from '@/shared/Button/Button' import { Input } from '@/shared/Input/Input' import { Modal } from '@/shared/Modal/Modal' import { MODAL_PARAMS_KEYS } from '@/types/modal' -import { Stack, Typography } from '@mui/material' -import React, { ChangeEvent, FormEvent, useState } from 'react' +import { CircularProgress, Stack, Typography, useTheme } from '@mui/material' import { StyledAppLogo } from './styled' import { useNavigate } from 'react-router-dom' +import { useForm } from 'react-hook-form' +import { FormInputType, schema } from './const' +import { yupResolver } from '@hookform/resolvers/yup' +import { usePassword } from '@/hooks/usePassword' +import { useCallback, useEffect, useState } from 'react' +import { useDebounce } from 'use-debounce' +import { fetchNip05 } from '@/utils/helpers/helpers' +import { DOMAIN } from '@/utils/consts' +import { CheckmarkIcon } from '@/assets' + +const FORM_DEFAULT_VALUES = { + username: '', + nsec: '', +} export const ModalImportKeys = () => { const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS) + const { hidePassword, inputProps } = usePassword() + const theme = useTheme() + + const { + handleSubmit, + reset, + register, + formState: { errors }, + watch, + } = useForm({ + defaultValues: FORM_DEFAULT_VALUES, + resolver: yupResolver(schema), + mode: 'onSubmit', + }) + const [isLoading, setIsLoading] = useState(false) + const [isAvailable, setIsAvailable] = useState(false) + const enteredUsername = watch('username') + const [debouncedUsername] = useDebounce(enteredUsername, 100) + + const checkIsUsernameAvailable = useCallback(async () => { + if (!debouncedUsername.trim().length) return undefined + const npubNip05 = await fetchNip05(`${debouncedUsername}@${DOMAIN}`) + + setIsAvailable(!npubNip05) + }, [debouncedUsername]) + + useEffect(() => { + checkIsUsernameAvailable() + }, [checkIsUsernameAvailable]) + + const cleanUpStates = useCallback(() => { + hidePassword() + reset() + setIsLoading(false) + setIsAvailable(false) + }, [reset, hidePassword]) const notify = useEnqueueSnackbar() const navigate = useNavigate() - const [enteredNsec, setEnteredNsec] = useState('') - - const handleNsecChange = (e: ChangeEvent) => { - setEnteredNsec(e.target.value) - } - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault() + const submitHandler = async (values: FormInputType) => { + if (isLoading || !isAvailable) return undefined try { - if (!enteredNsec.trim().length) return - const enteredName = '' // FIXME get from input - const k: any = await swicCall('importKey', enteredName, enteredNsec) + const { nsec, username } = values + setIsLoading(true) + const k: any = await swicCall('importKey', username, nsec) notify('Key imported!', 'success') navigate(`/key/${k.npub}`) + cleanUpStates() } catch (error: any) { - notify(error.message, 'error') + notify(error?.message || 'Something went wrong!', 'error') + cleanUpStates() } } + useEffect(() => { + return () => { + isModalOpened && cleanUpStates() + } + }, [isModalOpened, cleanUpStates]) + + const getInputHelperText = () => { + if (!enteredUsername) return "Don't worry, username can be changed later." + if (!isAvailable) return 'Already taken' + return ( + <> + Available + + ) + } + + const inputHelperText = getInputHelperText() + return ( - + Import keys + - + + ) diff --git a/src/components/Modal/ModalImportKeys/const.ts b/src/components/Modal/ModalImportKeys/const.ts new file mode 100644 index 0000000..c84d077 --- /dev/null +++ b/src/components/Modal/ModalImportKeys/const.ts @@ -0,0 +1,8 @@ +import * as yup from 'yup' + +export const schema = yup.object().shape({ + username: yup.string().required(), + nsec: yup.string().required(), +}) + +export type FormInputType = yup.InferType diff --git a/src/components/Modal/ModalLogin/ModalLogin.tsx b/src/components/Modal/ModalLogin/ModalLogin.tsx index 13d4e02..ea490c8 100644 --- a/src/components/Modal/ModalLogin/ModalLogin.tsx +++ b/src/components/Modal/ModalLogin/ModalLogin.tsx @@ -4,18 +4,23 @@ 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 { CircularProgress, Stack, Typography } from '@mui/material' import { StyledAppLogo } from './styled' import { Input } from '@/shared/Input/Input' 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' import { DOMAIN } from '@/utils/consts' import { fetchNip05 } from '@/utils/helpers/helpers' +import { usePassword } from '@/hooks/usePassword' +import { dbi } from '@/modules/db' + +const FORM_DEFAULT_VALUES = { + username: '', + password: '', +} export const ModalLogin = () => { const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() @@ -23,8 +28,9 @@ export const ModalLogin = () => { const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN) const notify = useEnqueueSnackbar() - const navigate = useNavigate() + const { hidePassword, inputProps } = usePassword() + const [isLoading, setIsLoading] = useState(false) const { handleSubmit, @@ -32,27 +38,25 @@ export const ModalLogin = () => { register, formState: { errors }, } = useForm({ - defaultValues: { - username: '', - password: '', - }, + defaultValues: FORM_DEFAULT_VALUES, resolver: yupResolver(schema), mode: 'onSubmit', }) - const [isPasswordShown, setIsPasswordShown] = useState(false) - - const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState) - const cleanUpStates = useCallback(() => { - setIsPasswordShown(false) + hidePassword() reset() - }, [reset]) + setIsLoading(false) + }, [reset, hidePassword]) const submitHandler = async (values: FormInputType) => { + if (isLoading) return undefined + try { + setIsLoading(true) let npub = values.username let name = '' + if (!npub.startsWith('npub1')) { name = npub if (!npub.includes('@')) { @@ -72,11 +76,13 @@ export const ModalLogin = () => { console.log('fetch', npub, name) const k: any = await swicCall('fetchKey', npub, passphrase, name) notify(`Fetched ${k.npub}`, 'success') + dbi.addSynced(k.npub) cleanUpStates() navigate(`/key/${k.npub}`) } catch (error: any) { console.log('error', error) notify(error?.message || 'Something went wrong!', 'error') + setIsLoading(false) } } @@ -110,16 +116,11 @@ export const ModalLogin = () => { fullWidth placeholder="Your password" {...register('password')} - endAdornment={ - - {isPasswordShown ? : } - - } - type={isPasswordShown ? 'text' : 'password'} + {...inputProps} error={!!errors.password} /> - diff --git a/src/components/Modal/ModalSettings/ModalSettings.tsx b/src/components/Modal/ModalSettings/ModalSettings.tsx index 9d59013..97a5502 100644 --- a/src/components/Modal/ModalSettings/ModalSettings.tsx +++ b/src/components/Modal/ModalSettings/ModalSettings.tsx @@ -2,19 +2,20 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { Button } from '@/shared/Button/Button' import { Modal } from '@/shared/Modal/Modal' import { MODAL_PARAMS_KEYS } from '@/types/modal' -import { Box, CircularProgress, IconButton, Stack, Typography } from '@mui/material' +import { Box, CircularProgress, Stack, Typography } from '@mui/material' import { StyledButton, StyledSettingContainer, StyledSynchedText } from './styled' import { SectionTitle } from '@/shared/SectionTitle/SectionTitle' import { CheckmarkIcon } from '@/assets' import { Input } from '@/shared/Input/Input' -import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined' -import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined' import { ChangeEvent, FC, useEffect, useState } from 'react' import { Checkbox } from '@/shared/Checkbox/Checkbox' import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' import { swicCall } from '@/modules/swic' import { useParams } from 'react-router-dom' import { dbi } from '@/modules/db' +import { usePassword } from '@/hooks/usePassword' +import { useAppSelector } from '@/store/hooks/redux' +import { selectKeys } from '@/store' type ModalSettingsProps = { isSynced: boolean @@ -23,29 +24,44 @@ type ModalSettingsProps = { export const ModalSettings: FC = ({ isSynced }) => { const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const { npub = '' } = useParams<{ npub: string }>() + const keys = useAppSelector(selectKeys) const notify = useEnqueueSnackbar() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS) + const { hidePassword, inputProps } = usePassword() + const [enteredPassword, setEnteredPassword] = useState('') - const [isPasswordShown, setIsPasswordShown] = useState(false) const [isPasswordInvalid, setIsPasswordInvalid] = useState(false) const [isChecked, setIsChecked] = useState(false) - const [isLoading, setIsLoading] = useState(false) useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced]) + useEffect(() => { + return () => { + if (isModalOpened) { + // modal closed + hidePassword() + } + } + }, [hidePassword, isModalOpened]) + + const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) + + if (isModalOpened && !isNpubExists) { + handleCloseModal() + return null + } + const handlePasswordChange = (e: ChangeEvent) => { setIsPasswordInvalid(false) setEnteredPassword(e.target.value) } - const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState) - const onClose = () => { handleCloseModal() setEnteredPassword('') @@ -95,16 +111,7 @@ export const ModalSettings: FC = ({ isSynced }) => { - {isPasswordShown ? ( - - ) : ( - - )} - - } - type={isPasswordShown ? 'text' : 'password'} + {...inputProps} onChange={handlePasswordChange} value={enteredPassword} helperText={isPasswordInvalid ? 'Invalid password' : ''} diff --git a/src/components/Modal/ModalSignUp/ModalSignUp.tsx b/src/components/Modal/ModalSignUp/ModalSignUp.tsx index ec9eaeb..0e5975b 100644 --- a/src/components/Modal/ModalSignUp/ModalSignUp.tsx +++ b/src/components/Modal/ModalSignUp/ModalSignUp.tsx @@ -2,8 +2,8 @@ import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { Modal } from '@/shared/Modal/Modal' import { MODAL_PARAMS_KEYS } from '@/types/modal' -import { Stack, Typography, useTheme } from '@mui/material' -import React, { ChangeEvent, useState } from 'react' +import { CircularProgress, Stack, Typography, useTheme } from '@mui/material' +import React, { ChangeEvent, useEffect, useState } from 'react' import { StyledAppLogo } from './styled' import { Input } from '@/shared/Input/Input' import { Button } from '@/shared/Button/Button' @@ -25,6 +25,8 @@ export const ModalSignUp = () => { const [enteredValue, setEnteredValue] = useState('') const [isAvailable, setIsAvailable] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const handleInputChange = async (e: ChangeEvent) => { setEnteredValue(e.target.value) const name = e.target.value.trim() @@ -36,31 +38,47 @@ export const ModalSignUp = () => { } } - const inputHelperText = enteredValue ? ( - isAvailable ? ( + const getInputHelperText = () => { + if (!enteredValue) return "Don't worry, username can be changed later." + if (!isAvailable) return 'Already taken' + return ( <> Available - ) : ( - <>Already taken ) - ) : ( - "Don't worry, username can be changed later." - ) + } + + const inputHelperText = getInputHelperText() const handleSubmit = async (e: React.FormEvent) => { + if (isLoading || !isAvailable) return undefined + const name = enteredValue.trim() if (!name.length) return e.preventDefault() + try { + setIsLoading(true) const k: any = await swicCall('generateKey', name) notify(`Account created for "${name}"`, 'success') navigate(`/key/${k.npub}`) + setIsLoading(false) } catch (error: any) { - notify(error.message, 'error') + notify(error?.message || 'Something went wrong!', 'error') + setIsLoading(false) } } + useEffect(() => { + return () => { + if (isModalOpened) { + // modal closed + setIsLoading(false) + setIsAvailable(false) + } + } + }, [isModalOpened]) + return ( @@ -91,8 +109,8 @@ export const ModalSignUp = () => { }, }} /> - diff --git a/src/hooks/usePassword.tsx b/src/hooks/usePassword.tsx new file mode 100644 index 0000000..39126ad --- /dev/null +++ b/src/hooks/usePassword.tsx @@ -0,0 +1,30 @@ +import { useCallback, useMemo, useState } from 'react' +import { IconButton } from '@mui/material' +import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined' +import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined' + +export const usePassword = () => { + const [isPasswordShown, setIsPasswordShown] = useState(false) + + const handlePasswordTypeChange = useCallback(() => setIsPasswordShown((prevState) => !prevState), []) + + const hidePassword = useCallback(() => setIsPasswordShown(false), []) + + const inputProps = useMemo( + () => ({ + endAdornment: ( + + {isPasswordShown ? ( + + ) : ( + + )} + + ), + type: isPasswordShown ? 'text' : 'password', + }), + [handlePasswordTypeChange, isPasswordShown] + ) + + return { inputProps, hidePassword } +} diff --git a/src/hooks/useProfile.ts b/src/hooks/useProfile.ts index c3b4554..6b7d7e9 100644 --- a/src/hooks/useProfile.ts +++ b/src/hooks/useProfile.ts @@ -6,35 +6,36 @@ import { useAppSelector } from '@/store/hooks/redux' import { selectKeyByNpub } from '@/store' const getFirstLetter = (text: string | undefined): string | null => { - if (!text || text.trim().length === 0) return null - return text.substring(0, 1).toUpperCase() + if (!text || text.trim().length === 0) return null + return text.substring(0, 1).toUpperCase() } export const useProfile = (npub: string) => { - const [profile, setProfile] = useState(null) - const currentKey = useAppSelector((state) => selectKeyByNpub(state, npub)) + const [profile, setProfile] = useState(null) + const currentKey = useAppSelector((state) => selectKeyByNpub(state, npub)) - const userName = getProfileUsername(profile) || currentKey?.name - const userAvatar = profile?.info?.picture || '' - const avatarTitle = getFirstLetter(userName) + const userName = getProfileUsername(profile) || currentKey?.name + const userAvatar = profile?.info?.picture || '' + const avatarTitle = getFirstLetter(userName) - const loadProfile = useCallback(async () => { - try { - const response = await fetchProfile(npub) - setProfile(response) - } catch (error) { - console.error('Failed to fetch profile:', error) - } - }, [npub]) + const loadProfile = useCallback(async () => { + if (!npub) return undefined + try { + const response = await fetchProfile(npub) + setProfile(response) + } catch (error) { + console.error('Failed to fetch profile:', error) + } + }, [npub]) - useEffect(() => { - loadProfile() - }, [loadProfile]) + useEffect(() => { + loadProfile() + }, [loadProfile]) - return { - profile, - userName: userName || getShortenNpub(npub), - userAvatar, - avatarTitle, - } + return { + profile, + userName: userName || getShortenNpub(npub), + userAvatar, + avatarTitle, + } } diff --git a/src/layout/Header/Header.tsx b/src/layout/Header/Header.tsx index 72b39ec..67f66f4 100644 --- a/src/layout/Header/Header.tsx +++ b/src/layout/Header/Header.tsx @@ -1,35 +1,48 @@ import { Avatar, Stack, Toolbar, Typography } from '@mui/material' import { AppLogo } from '../../assets' -import { StyledAppBar, StyledAppName } from './styled' +import { StyledAppBar, StyledAppName, StyledProfileContainer, StyledThemeButton } from './styled' import { Menu } from './components/Menu' import { useNavigate, useParams } from 'react-router-dom' import { ProfileMenu } from './components/ProfileMenu' import { useProfile } from '@/hooks/useProfile' +import DarkModeIcon from '@mui/icons-material/DarkMode' +import LightModeIcon from '@mui/icons-material/LightMode' +import { useAppDispatch, useAppSelector } from '@/store/hooks/redux' +import { setThemeMode } from '@/store/reducers/ui.slice' export const Header = () => { + const themeMode = useAppSelector((state) => state.ui.themeMode) + const navigate = useNavigate() + const dispatch = useAppDispatch() + const { npub = '' } = useParams<{ npub: string }>() const { userName, userAvatar, avatarTitle } = useProfile(npub) const showProfile = Boolean(npub) - const navigate = useNavigate() - const handleNavigate = () => { navigate(`/key/${npub}`) } + const isDarkMode = themeMode === 'dark' + const themeIcon = isDarkMode ? : + + const handleChangeMode = () => { + dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' })) + } + return ( {showProfile && ( - - + + {avatarTitle} - + {userName} - + )} {!showProfile && ( @@ -39,6 +52,8 @@ export const Header = () => { )} + {themeIcon} + {showProfile ? : } diff --git a/src/layout/Header/components/Menu.tsx b/src/layout/Header/components/Menu.tsx index 538795a..f7be0e0 100644 --- a/src/layout/Header/components/Menu.tsx +++ b/src/layout/Header/components/Menu.tsx @@ -1,10 +1,7 @@ import { Menu as MuiMenu } from '@mui/material' -import DarkModeIcon from '@mui/icons-material/DarkMode' -import LightModeIcon from '@mui/icons-material/LightMode' import LoginIcon from '@mui/icons-material/Login' import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded' -import { setThemeMode } from '@/store/reducers/ui.slice' -import { useAppDispatch, useAppSelector } from '@/store/hooks/redux' +import { useAppSelector } from '@/store/hooks/redux' import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MenuButton } from './styled' @@ -14,26 +11,17 @@ import MenuRoundedIcon from '@mui/icons-material/MenuRounded' import { selectKeys } from '@/store' export const Menu = () => { - const themeMode = useAppSelector((state) => state.ui.themeMode) const keys = useAppSelector(selectKeys) - const dispatch = useAppDispatch() const { handleOpen } = useModalSearchParams() - - const isDarkMode = themeMode === 'dark' - const isNoKeys = !keys || keys.length === 0 - const { anchorEl, handleClose, handleOpen: handleOpenMenu, open } = useOpenMenu() - const handleChangeMode = () => { - dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' })) - } + const isNoKeys = !keys || keys.length === 0 + const handleNavigateToAuth = () => { handleOpen(MODAL_PARAMS_KEYS.INITIAL) handleClose() } - const themeIcon = isDarkMode ? : - return ( <> @@ -52,7 +40,6 @@ export const Menu = () => { onClick={handleNavigateToAuth} title={isNoKeys ? 'Sign up' : 'Add account'} /> - ) diff --git a/src/layout/Header/components/ProfileMenu.tsx b/src/layout/Header/components/ProfileMenu.tsx index 42e7468..e58bae1 100644 --- a/src/layout/Header/components/ProfileMenu.tsx +++ b/src/layout/Header/components/ProfileMenu.tsx @@ -9,11 +9,9 @@ import LoginIcon from '@mui/icons-material/Login' import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded' import HomeRoundedIcon from '@mui/icons-material/HomeRounded' import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded' -import { useAppDispatch, useAppSelector } from '@/store/hooks/redux' +import { useAppSelector } from '@/store/hooks/redux' import { selectKeys } from '@/store' -import { setThemeMode } from '@/store/reducers/ui.slice' -import DarkModeIcon from '@mui/icons-material/DarkMode' -import LightModeIcon from '@mui/icons-material/LightMode' + import { ListProfiles } from './ListProfiles' import { DbKey } from '@/modules/db' @@ -23,10 +21,7 @@ export const ProfileMenu = () => { const keys = useAppSelector(selectKeys) const isNoKeys = !keys || keys.length === 0 - const themeMode = useAppSelector((state) => state.ui.themeMode) - const isDarkMode = themeMode === 'dark' - const dispatch = useAppDispatch() const navigate = useNavigate() const handleNavigateToAuth = () => { @@ -39,17 +34,11 @@ export const ProfileMenu = () => { handleClose() } - const handleChangeMode = () => { - dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' })) - } - const handleNavigateToKeyInnerPage = (key: DbKey) => { navigate('/key/' + key.npub) handleClose() } - const themeIcon = isDarkMode ? : - return ( <> @@ -71,7 +60,6 @@ export const ProfileMenu = () => { onClick={handleNavigateToAuth} title={isNoKeys ? 'Sign up' : 'Add account'} /> - ) diff --git a/src/layout/Header/styled.tsx b/src/layout/Header/styled.tsx index 8dd5500..5f08d8b 100644 --- a/src/layout/Header/styled.tsx +++ b/src/layout/Header/styled.tsx @@ -1,4 +1,4 @@ -import { AppBar, Typography, TypographyProps, styled } from '@mui/material' +import { AppBar, IconButton, Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material' import { Link } from 'react-router-dom' export const StyledAppBar = styled(AppBar)(({ theme }) => { @@ -11,6 +11,7 @@ export const StyledAppBar = styled(AppBar)(({ theme }) => { maxWidth: 'inherit', left: '50%', transform: 'translateX(-50%)', + borderRadius: '8px', } }) @@ -29,3 +30,23 @@ export const StyledAppName = styled((props: TypographyProps) => ( lineHeight: '22.4px', marginLeft: '0.5rem', })) + +export const StyledProfileContainer = styled((props: StackProps) => )(() => ({ + gap: '1rem', + flexDirection: 'row', + alignItems: 'center', + flex: 1, + '& .avatar': { + cursor: 'pointer', + }, + '& .username': { + cursor: 'pointer', + '&:hover': { + textDecoration: 'underline', + }, + }, +})) + +export const StyledThemeButton = styled(IconButton)({ + margin: '0 0.5rem', +}) diff --git a/src/modules/db.ts b/src/modules/db.ts index c776bc2..5fbbf76 100644 --- a/src/modules/db.ts +++ b/src/modules/db.ts @@ -103,6 +103,17 @@ export const dbi = { console.log(`db addApp error: ${error}`) } }, + updateApp: async (app: Omit) => { + try { + await db.apps.where({ appNpub: app.appNpub }).modify({ + name: app.name, + icon: app.icon, + url: app.url, + }) + } catch (error) { + console.log(`db updateApp error: ${error}`) + } + }, listApps: async (): Promise => { try { return await db.apps.toArray() diff --git a/src/pages/AppPage/App.Page.tsx b/src/pages/AppPage/App.Page.tsx index 1aaf957..715ccb1 100644 --- a/src/pages/AppPage/App.Page.tsx +++ b/src/pages/AppPage/App.Page.tsx @@ -1,9 +1,9 @@ import { useParams } from 'react-router' import { useAppSelector } from '@/store/hooks/redux' -import { selectAppByAppNpub, selectPermsByNpubAndAppNpub } from '@/store' +import { selectAppByAppNpub, selectKeys, selectPermsByNpubAndAppNpub } from '@/store' import { Navigate, useNavigate } from 'react-router-dom' import { formatTimestampDate } from '@/utils/helpers/date' -import { Box, Stack, Typography } from '@mui/material' +import { Box, IconButton, Stack, Typography } from '@mui/material' import { SectionTitle } from '@/shared/SectionTitle/SectionTitle' import { getAppIconTitle, getShortenNpub } from '@/utils/helpers/helpers' import { Button } from '@/shared/Button/Button' @@ -18,30 +18,34 @@ import { IOSBackButton } from '@/shared/IOSBackButton/IOSBackButton' import { ModalActivities } from './components/Activities/ModalActivities' import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { MODAL_PARAMS_KEYS } from '@/types/modal' +import MoreIcon from '@mui/icons-material/MoreVertRounded' +import { ModalAppDetails } from '@/components/Modal/ModalAppDetails/ModalAppDetails' const AppPage = () => { + const keys = useAppSelector(selectKeys) + const { appNpub = '', npub = '' } = useParams() + const currentApp = useAppSelector((state) => selectAppByAppNpub(state, appNpub)) + const perms = useAppSelector((state) => selectPermsByNpubAndAppNpub(state, npub, appNpub)) + const navigate = useNavigate() const notify = useEnqueueSnackbar() - - const perms = useAppSelector((state) => selectPermsByNpubAndAppNpub(state, npub, appNpub)) - const currentApp = useAppSelector((state) => selectAppByAppNpub(state, appNpub)) - const { open, handleClose, handleShow } = useToggleConfirm() const { handleOpen: handleOpenModal } = useModalSearchParams() const connectPerm = perms.find((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC) - if (!currentApp) { + const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) + + if (!isNpubExists || !currentApp) { return } const { icon = '', name = '' } = currentApp || {} const appName = name || getShortenNpub(appNpub) - const appAvatarTitle = getAppIconTitle(name, appNpub) + const appAvatarTitle = getAppIconTitle(name, appNpub) const { timestamp } = connectPerm || {} - const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected' const handleDeleteApp = async () => { @@ -54,18 +58,23 @@ const AppPage = () => { } } + const handleShowAppDetailsModal = () => handleOpenModal(MODAL_PARAMS_KEYS.APP_DETAILS) + return ( <> navigate(`key/${npub}`)} /> - - {appAvatarTitle} - + {appAvatarTitle} - - {appName} - + + + {appName} + + + + + {connectedOn} @@ -93,6 +102,7 @@ const AppPage = () => { onClose={handleClose} /> + ) } diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx index cfa968f..d39ebd2 100644 --- a/src/pages/KeyPage/Key.Page.tsx +++ b/src/pages/KeyPage/Key.Page.tsx @@ -1,5 +1,5 @@ import { useAppSelector } from '../../store/hooks/redux' -import { useParams } from 'react-router-dom' +import { Navigate, useParams } from 'react-router-dom' import { Stack } from '@mui/material' import { StyledIconButton } from './styled' import { SettingsIcon, ShareIcon } from '@/assets' @@ -18,29 +18,32 @@ import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal' import { useLiveQuery } from 'dexie-react-hooks' import { checkNpubSyncQuerier } from './utils' import { DOMAIN } from '@/utils/consts' +import { useCallback } from 'react' const KeyPage = () => { const { npub = '' } = useParams<{ npub: string }>() const { keys, apps, pending, perms } = useAppSelector((state) => state.content) + const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false) - const { handleOpen } = useModalSearchParams() - const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning() const key = keys.find((k) => k.npub === npub) - let username = '' - if (key?.name) { - if (key.name.includes('@')) username = key.name - else username = `${key?.name}@${DOMAIN}` - } + const getUsername = useCallback(() => { + if (!key || !key?.name) return '' + if (key.name.includes('@')) return key.name + return `${key?.name}@${DOMAIN}` + }, [key]) + const username = getUsername() const filteredApps = apps.filter((a) => a.npub === npub) const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms) - const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP) + const isKeyExists = npub.trim().length && key + if (!isKeyExists) return + const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP) const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS) return ( diff --git a/src/pages/KeyPage/components/styled.tsx b/src/pages/KeyPage/components/styled.tsx index 7f53e81..420c91c 100644 --- a/src/pages/KeyPage/components/styled.tsx +++ b/src/pages/KeyPage/components/styled.tsx @@ -1,9 +1,9 @@ -import { Input, InputProps } from '@/shared/Input/Input' +import { Input, AppInputProps } from '@/shared/Input/Input' import { Stack, StackProps, styled } from '@mui/material' import { forwardRef } from 'react' export const StyledInput = styled( - forwardRef(({ className, ...props }, ref) => { + forwardRef(({ className, ...props }, ref) => { return ( { }) export const StyledInput = styled( - forwardRef(({ className, ...props }, ref) => { + forwardRef(({ className, ...props }, ref) => { return ( void debounceTimeout: number } -export const DebounceInput = (props: InputProps & DebounceProps) => { +export const DebounceInput = (props: AppInputProps & DebounceProps) => { const { handleDebounce, debounceTimeout, ...rest } = props const timerRef = useRef() diff --git a/src/shared/Input/Input.tsx b/src/shared/Input/Input.tsx index 12a861f..9cb6f74 100644 --- a/src/shared/Input/Input.tsx +++ b/src/shared/Input/Input.tsx @@ -10,14 +10,14 @@ import { styled, } from '@mui/material' -export type InputProps = InputBaseProps & { +export type AppInputProps = InputBaseProps & { helperText?: string | ReactNode helperTextProps?: FormHelperTextProps containerProps?: BoxProps label?: string } -export const Input = forwardRef( +export const Input = forwardRef( ({ helperText, containerProps, helperTextProps, label, ...props }, ref) => { return ( @@ -26,7 +26,7 @@ export const Input = forwardRef( {label} ) : null} - + {helperText ? ( {helperText} diff --git a/src/shared/Modal/Modal.tsx b/src/shared/Modal/Modal.tsx index 6547cde..069b10a 100644 --- a/src/shared/Modal/Modal.tsx +++ b/src/shared/Modal/Modal.tsx @@ -20,7 +20,7 @@ const Transition = forwardRef(function Transition( export const Modal: FC = ({ children, title, onClose, withCloseButton = true, fixedHeight, ...props }) => { return ( - + {withCloseButton && ( onClose && onClose({}, 'backdropClick')} className="close_btn"> diff --git a/src/shared/Modal/styled.tsx b/src/shared/Modal/styled.tsx index 3ef0ce0..076402b 100644 --- a/src/shared/Modal/styled.tsx +++ b/src/shared/Modal/styled.tsx @@ -10,7 +10,7 @@ import { styled, } from '@mui/material' -export const StyledDialog = styled((props: DialogProps & { fixedHeight?: string }) => ( +export const StyledDialog = styled((props: DialogProps & { fixedheight?: string }) => ( -))(({ theme, fixedHeight = '' }) => { - const fixedHeightStyles = fixedHeight ? { height: fixedHeight } : {} +))(({ theme, fixedheight = '' }) => { + const fixedHeightStyles = fixedheight ? { height: fixedheight } : {} return { '& .container': { alignItems: 'flex-end', diff --git a/src/store/index.ts b/src/store/index.ts index 1b2ba2b..b95e9df 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -43,9 +43,10 @@ export type RootState = ReturnType export type AppDispatch = typeof store.dispatch export const selectKeys = (state: RootState) => state.content.keys +export const selectApps = (state: RootState) => state.content.apps export const selectKeyByNpub = (state: RootState, npub: string) => { - return state.content.keys.find((key) => key.npub === npub) + return state.content.keys.find((key) => key.npub === npub) } export const selectAppsByNpub = memoizeOne((state: RootState, npub: string) => { diff --git a/src/types/modal.ts b/src/types/modal.ts index ad18490..df950c3 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -9,6 +9,7 @@ export enum MODAL_PARAMS_KEYS { CONFIRM_CONNECT = 'confirm-connect', CONFIRM_EVENT = 'confirm-event', ACTIVITY = 'activity', + APP_DETAILS = 'app-details', } export enum EXPLANATION_MODAL_KEYS { diff --git a/src/utils/helpers/helpers.ts b/src/utils/helpers/helpers.ts index 32c7019..1ffba4b 100644 --- a/src/utils/helpers/helpers.ts +++ b/src/utils/helpers/helpers.ts @@ -127,3 +127,7 @@ export function getPermActionName(req: DbPerm) { } return action } + +export const isEmptyString = (str = '') => { + return str.trim().length === 0 +} From 2b6a1e1e5d96454f08523f696486fde80cdf902d Mon Sep 17 00:00:00 2001 From: artur Date: Fri, 9 Feb 2024 15:07:16 +0300 Subject: [PATCH 3/5] Fix check of pending req id on connect modal --- .../Modal/ModalConfirmConnect/ModalConfirmConnect.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx index 3079fad..37ae255 100644 --- a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx +++ b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx @@ -5,7 +5,7 @@ import { call, getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helper import { Avatar, Box, Stack, Typography } from '@mui/material' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { useAppSelector } from '@/store/hooks/redux' -import { selectAppsByNpub, selectKeys } from '@/store' +import { selectAppsByNpub, selectKeys, selectPendingsByNpub } from '@/store' import { StyledButton, StyledToggleButtonsGroup } from './styled' import { ActionToggleButton } from './сomponents/ActionToggleButton' import { useState } from 'react' @@ -25,6 +25,7 @@ export const ModalConfirmConnect = () => { const paramNpub = searchParams.get('npub') || '' const { npub = paramNpub } = useParams<{ npub: string }>() const apps = useAppSelector((state) => selectAppsByNpub(state, npub)) + const pending = useAppSelector((state) => selectPendingsByNpub(state, npub)) const [selectedActionType, setSelectedActionType] = useState(ACTION_TYPE.BASIC) @@ -62,7 +63,7 @@ export const ModalConfirmConnect = () => { const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub) - const isPendingReqIdExists = pendingReqId.trim().length + const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId) if (isModalOpened && (!isNpubExists || !isAppNpubExists || !isPendingReqIdExists)) { closeModalAfterRequest() return null From f408fd1b3861614bcd3b9f68755875b690c3592f Mon Sep 17 00:00:00 2001 From: Bekbolsun Date: Fri, 9 Feb 2024 19:33:32 +0600 Subject: [PATCH 4/5] fix reload on submit, button disabled styles, profile name styles in header --- .../Modal/ModalAppDetails/ModalAppDetails.tsx | 4 ++-- .../Modal/ModalImportKeys/ModalImportKeys.tsx | 5 +++-- .../Modal/ModalSignUp/ModalSignUp.tsx | 6 +++--- src/layout/Header/styled.tsx | 3 --- src/shared/Button/Button.tsx | 17 ++++++++++------- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx b/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx index ca3ce23..37f09cb 100644 --- a/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx +++ b/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx @@ -43,7 +43,7 @@ export const ModalAppDetails = () => { }) // eslint-disable-next-line - }, [appNpub]) + }, [appNpub, isModalOpened]) useEffect(() => { return () => { @@ -164,7 +164,7 @@ export const ModalAppDetails = () => { value={details.icon} /> - diff --git a/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx b/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx index 153262c..5cf16df 100644 --- a/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx +++ b/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx @@ -110,9 +110,10 @@ export const ModalImportKeys = () => { @{DOMAIN}} {...register('username')} error={!!errors.username} helperText={inputHelperText} diff --git a/src/components/Modal/ModalSignUp/ModalSignUp.tsx b/src/components/Modal/ModalSignUp/ModalSignUp.tsx index 0e5975b..b63c296 100644 --- a/src/components/Modal/ModalSignUp/ModalSignUp.tsx +++ b/src/components/Modal/ModalSignUp/ModalSignUp.tsx @@ -51,11 +51,11 @@ export const ModalSignUp = () => { const inputHelperText = getInputHelperText() const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() if (isLoading || !isAvailable) return undefined const name = enteredValue.trim() if (!name.length) return - e.preventDefault() try { setIsLoading(true) @@ -89,9 +89,9 @@ export const ModalSignUp = () => { @{DOMAIN}} onChange={handleInputChange} diff --git a/src/layout/Header/styled.tsx b/src/layout/Header/styled.tsx index 5f08d8b..3eee562 100644 --- a/src/layout/Header/styled.tsx +++ b/src/layout/Header/styled.tsx @@ -41,9 +41,6 @@ export const StyledProfileContainer = styled((props: StackProps) => (({ children, ...restProps }, ref) => { return ( - + {children} ) @@ -27,19 +27,22 @@ const StyledButton = styled( background: theme.palette.backgroundSecondary.default, }, color: theme.palette.text.primary, + '&.disabled': { + opacity: 0.5, + cursor: 'not-allowed', + }, } } return { ...commonStyles, - '&.button:is(:hover, :active, &)': { + '&.button:is(:hover, :active, &, .disabled)': { background: theme.palette.primary.main, }, color: theme.palette.text.secondary, - ':disabled': { - '&.button:is(:hover, :active, &)': { - background: theme.palette.backgroundSecondary.default, - }, - color: theme.palette.backgroundSecondary.paper, + '&.disabled': { + color: theme.palette.text.secondary, + opacity: 0.5, + cursor: 'not-allowed', }, } }) From ec544a059252edf12f3d1d310149e6a5eff7b025 Mon Sep 17 00:00:00 2001 From: artur Date: Mon, 12 Feb 2024 10:26:21 +0300 Subject: [PATCH 5/5] Add explanations, make login name lowercase, add nostrapp link --- .../ModalConfirmConnect.tsx | 2 +- .../Modal/ModalConnectApp/ModalConnectApp.tsx | 7 +- .../ModalExplanation/ModalExplanation.tsx | 65 +++++++++++++++++-- .../Modal/ModalImportKeys/ModalImportKeys.tsx | 18 +++-- .../Modal/ModalInitial/ModalInitial.tsx | 2 +- src/pages/KeyPage/Key.Page.tsx | 2 +- src/pages/KeyPage/components/Apps.tsx | 8 ++- src/types/modal.ts | 1 + src/utils/helpers/helpers.ts | 2 +- 9 files changed, 89 insertions(+), 18 deletions(-) diff --git a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx index 37ae255..e52cbeb 100644 --- a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx +++ b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx @@ -38,7 +38,7 @@ export const ModalConfirmConnect = () => { const { name, url = '', icon = '' } = triggerApp || {} let appUrl = url || searchParams.get('appUrl') || '' - console.log('referrer', window.document.referrer, appUrl) + // console.log('referrer', window.document.referrer, appUrl) if (!appUrl && window.document.referrer) { try { const u = new URL(window.document.referrer) diff --git a/src/components/Modal/ModalConnectApp/ModalConnectApp.tsx b/src/components/Modal/ModalConnectApp/ModalConnectApp.tsx index e820134..bcc4e42 100644 --- a/src/components/Modal/ModalConnectApp/ModalConnectApp.tsx +++ b/src/components/Modal/ModalConnectApp/ModalConnectApp.tsx @@ -7,7 +7,7 @@ import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton' import { Modal } from '@/shared/Modal/Modal' import { selectKeys } from '@/store' import { useAppSelector } from '@/store/hooks/redux' -import { MODAL_PARAMS_KEYS } from '@/types/modal' +import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal' import { getBunkerLink } from '@/utils/helpers/helpers' import { Stack, Typography } from '@mui/material' import { useRef } from 'react' @@ -69,7 +69,10 @@ export const ModalConnectApp = () => { value={bunkerStr} endAdornment={} /> - handleOpen(MODAL_PARAMS_KEYS.EXPLANATION)} /> + handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, { search: { type: EXPLANATION_MODAL_KEYS.BUNKER } })} + /> diff --git a/src/components/Modal/ModalExplanation/ModalExplanation.tsx b/src/components/Modal/ModalExplanation/ModalExplanation.tsx index 2a788ff..f0153a4 100644 --- a/src/components/Modal/ModalExplanation/ModalExplanation.tsx +++ b/src/components/Modal/ModalExplanation/ModalExplanation.tsx @@ -1,7 +1,7 @@ import { FC } from 'react' import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { Modal } from '@/shared/Modal/Modal' -import { MODAL_PARAMS_KEYS } from '@/types/modal' +import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal' import { Stack, Typography } from '@mui/material' import { Button } from '@/shared/Button/Button' import { useSearchParams } from 'react-router-dom' @@ -10,7 +10,7 @@ type ModalExplanationProps = { explanationText?: string } -export const ModalExplanation: FC = ({ explanationText = '' }) => { +export const ModalExplanation: FC = () => { const { getModalOpened } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION) const [searchParams, setSearchParams] = useSearchParams() @@ -18,21 +18,76 @@ export const ModalExplanation: FC = ({ explanationText = const handleCloseModal = () => { searchParams.delete('type') searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION) - setSearchParams(searchParams) + setSearchParams(searchParams, { replace: true }) } + const type = searchParams.get('type') + + let title = '' + let explanationText + switch (type) { + case EXPLANATION_MODAL_KEYS.NPUB: { + title = 'What is NPUB?' + explanationText = ( + <> + NPUB is your Nostr PUBlic key. +
+
+ It is your global unique identifier on the Nostr network, and is derived from your private key. +
+
+ You can share your NPUB with other people so that they could unambiguously find you on the network. + + ) + break + } + case EXPLANATION_MODAL_KEYS.LOGIN: { + title = 'What is Login?' + explanationText = ( + <> + Login (username) is your human-readable name on the Nostr network. +
+
+ Unlike your NPUB, which is a long string of random symbols, your login is a meaningful name tied to a website + address (like name@nsec.app). +
+
+ Use your username to log in to Nostr apps. +
+
+ You can have many usernames all pointing to your NPUB. People also refer to these names as nostr-addresses or + NIP05 names. + + ) + break + } + case EXPLANATION_MODAL_KEYS.BUNKER: { + title = 'What is Bunker URL?' + explanationText = ( + <> + Bunker URL is a string used to connect to Nostr apps. +
+
+ Some apps require bunker URL to connect to your keys. Paste it to the app and then confirm a connection + request. + + ) + break + } + } return ( - + {explanationText} diff --git a/src/components/Modal/ModalInitial/ModalInitial.tsx b/src/components/Modal/ModalInitial/ModalInitial.tsx index 01cbcfa..98fa617 100644 --- a/src/components/Modal/ModalInitial/ModalInitial.tsx +++ b/src/components/Modal/ModalInitial/ModalInitial.tsx @@ -35,7 +35,7 @@ export const ModalInitial = () => { {showAdvancedContent && ( - + )}
diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx index d39ebd2..c08c65f 100644 --- a/src/pages/KeyPage/Key.Page.tsx +++ b/src/pages/KeyPage/Key.Page.tsx @@ -56,7 +56,7 @@ const KeyPage = () => { title="Your login" value={username} copyValue={username} - explanationType={EXPLANATION_MODAL_KEYS.NPUB} + explanationType={EXPLANATION_MODAL_KEYS.LOGIN} /> = ({ apps = [] }) => { }) } + const openAppStore = () => { + window.open('https://nostrapp.link', '_blank') + } + return ( Connected apps - + {!apps.length && ( No connected apps - + )} diff --git a/src/types/modal.ts b/src/types/modal.ts index df950c3..bf50959 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -15,4 +15,5 @@ export enum MODAL_PARAMS_KEYS { export enum EXPLANATION_MODAL_KEYS { BUNKER = 'bunker', NPUB = 'npub', + LOGIN = 'login', } diff --git a/src/utils/helpers/helpers.ts b/src/utils/helpers/helpers.ts index 1ffba4b..72fed38 100644 --- a/src/utils/helpers/helpers.ts +++ b/src/utils/helpers/helpers.ts @@ -80,7 +80,7 @@ export function isPackagePerm(perm: string, reqPerm: string) { export async function fetchNip05(value: string, origin?: string) { try { - const [username, domain] = value.split('@') + const [username, domain] = value.toLocaleLowerCase().split('@') if (!origin) origin = `https://${domain}` const response = await fetch(`${origin}/.well-known/nostr.json?name=${username}`) const getNpub: {