diff --git a/.env b/.env index d921aa0..30ed0af 100644 --- a/.env +++ b/.env @@ -2,4 +2,5 @@ # change if you're using a different noauthd server REACT_APP_WEB_PUSH_PUBKEY=BNW_39YcKbV4KunFxFhvMW5JUs8AljfFnGUeZpaerO-gwCoWyQat5ol0xOGB8MLaqqCbz0iptd2Qv3SToSGynMk #REACT_APP_NOAUTHD_URL=http://localhost:8000 -REACT_APP_NOAUTHD_URL=https://noauthd.login.nostrapps.org \ No newline at end of file +REACT_APP_NOAUTHD_URL=https://noauthd.login.nostrapps.org +REACT_APP_DOMAIN=nsec.app \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 3b8c205..3c8a329 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { DbKey, DbPending, dbi } from './modules/db' +import { DbKey, dbi } from './modules/db' import { useCallback, useEffect, useState } from 'react' import { swicOnRender } from './modules/swic' import { useAppDispatch } from './store/hooks/redux' @@ -65,18 +65,14 @@ function App() { dispatch(setPending({ pending })) // rerender -// setRender((r) => r + 1) - - if (!keys.length) - handleOpen(MODAL_PARAMS_KEYS.INITIAL) + // setRender((r) => r + 1) + if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL) + // eslint-disable-next-line }, [dispatch]) useEffect(() => { - console.log('NDK is connected', isConnected) - if (isConnected) { - load() - } + if (isConnected) load() }, [render, isConnected, load]) useEffect(() => { diff --git a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx index 65be41d..e0c9549 100644 --- a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx +++ b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx @@ -14,7 +14,7 @@ import { ACTION_TYPE } from '@/utils/consts' export const ModalConfirmConnect = () => { - const { getModalOpened, handleClose } = useModalSearchParams() + const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT) const { npub = '' } = useParams<{ npub: string }>() @@ -37,20 +37,24 @@ export const ModalConfirmConnect = () => { return setSelectedActionType(value) } - const handleCloseModal = handleClose( + const handleCloseModal = createHandleCloseReplace( MODAL_PARAMS_KEYS.CONFIRM_CONNECT, - async (sp) => { - sp.delete('appNpub') - sp.delete('reqId') - await swicCall('confirm', pendingReqId, false, false) + { + onClose: async (sp) => { + sp.delete('appNpub') + sp.delete('reqId') + await swicCall('confirm', pendingReqId, false, false) + } }, ) - const closeModalAfterRequest = handleClose( + const closeModalAfterRequest = createHandleCloseReplace( MODAL_PARAMS_KEYS.CONFIRM_CONNECT, - (sp) => { - sp.delete('appNpub') - sp.delete('reqId') - }, + { + onClose: (sp) => { + sp.delete('appNpub') + sp.delete('reqId') + }, + } ) async function confirmPending( diff --git a/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx b/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx index 26985c1..85ae45e 100644 --- a/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx +++ b/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx @@ -24,9 +24,10 @@ import { } from './styled' import { SectionTitle } from '@/shared/SectionTitle/SectionTitle' import { swicCall } from '@/modules/swic' -import { IPendingsByAppNpub } from '@/pages/KeyPage/Key.Page' import { Checkbox } from '@/shared/Checkbox/Checkbox' import { DbPending } from '@/modules/db' +import { ACTIONS } from '@/utils/consts' +import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal' enum ACTION_TYPE { ALWAYS = 'ALWAYS', @@ -44,20 +45,12 @@ type ModalConfirmEventProps = { confirmEventReqs: IPendingsByAppNpub } -export const ACTIONS: { [type: string]: string } = { - get_public_key: 'Get public key', - sign_event: 'Sign event', - connect: 'Connect', - nip04_encrypt: 'Encrypt message', - nip04_decrypt: 'Decrypt message', -} - type PendingRequest = DbPending & { checked: boolean } export const ModalConfirmEvent: FC = ({ confirmEventReqs, }) => { - const { getModalOpened, handleClose } = useModalSearchParams() + const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT) const [searchParams] = useSearchParams() @@ -93,23 +86,27 @@ export const ModalConfirmEvent: FC = ({ const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked) - const handleCloseModal = handleClose( + const handleCloseModal = createHandleCloseReplace( MODAL_PARAMS_KEYS.CONFIRM_EVENT, - (sp) => { - sp.delete('appNpub') - sp.delete('reqId') - selectedPendingRequests.forEach( - async (req) => await swicCall('confirm', req.id, false, false), - ) - }, + { + onClose: (sp) => { + sp.delete('appNpub') + sp.delete('reqId') + selectedPendingRequests.forEach( + async (req) => await swicCall('confirm', req.id, false, false), + ) + } + } ) - const closeModalAfterRequest = handleClose( + const closeModalAfterRequest = createHandleCloseReplace( MODAL_PARAMS_KEYS.CONFIRM_EVENT, - (sp) => { - sp.delete('appNpub') - sp.delete('reqId') - }, + { + onClose: (sp) => { + sp.delete('appNpub') + sp.delete('reqId') + } + } ) async function confirmPending(allow: boolean) { @@ -173,7 +170,7 @@ export const ModalConfirmEvent: FC = ({ {pendingRequests.map((req) => { return ( - + { - const { getModalOpened, handleClose, handleOpen } = useModalSearchParams() + const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams() const timerRef = useRef() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP) - const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.CONNECT_APP, () => { - clearTimeout(timerRef.current) - }) + const handleCloseModal = createHandleCloseReplace( + MODAL_PARAMS_KEYS.CONNECT_APP, + { + onClose: () => { + clearTimeout(timerRef.current) + } + } + ) const notify = useEnqueueSnackbar() diff --git a/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx b/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx index 2852081..216353d 100644 --- a/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx +++ b/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx @@ -8,13 +8,15 @@ import { MODAL_PARAMS_KEYS } from '@/types/modal' import { Stack, Typography } from '@mui/material' import React, { ChangeEvent, FormEvent, useState } from 'react' import { StyledAppLogo } from './styled' +import { useNavigate } from 'react-router-dom' export const ModalImportKeys = () => { - const { getModalOpened, handleClose } = useModalSearchParams() + const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS) - const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.IMPORT_KEYS) + const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS) const notify = useEnqueueSnackbar() + const navigate = useNavigate() const [enteredNsec, setEnteredNsec] = useState('') @@ -26,9 +28,9 @@ export const ModalImportKeys = () => { e.preventDefault() try { if (!enteredNsec.trim().length) return - await swicCall('importKey', enteredNsec) + const k: any = await swicCall('importKey', enteredNsec) notify('Key imported!', 'success') - handleCloseModal() + navigate(`/key/${k.npub}`) } catch (error: any) { notify(error.message, 'error') } @@ -36,12 +38,7 @@ export const ModalImportKeys = () => { return ( - + { value={enteredNsec} onChange={handleNsecChange} fullWidth + type='password' /> - + ) diff --git a/src/components/Modal/ModalInitial/ModalInitial.tsx b/src/components/Modal/ModalInitial/ModalInitial.tsx index b89d879..15b010a 100644 --- a/src/components/Modal/ModalInitial/ModalInitial.tsx +++ b/src/components/Modal/ModalInitial/ModalInitial.tsx @@ -7,10 +7,10 @@ import { Fade, Stack } from '@mui/material' import { AppLink } from '@/shared/AppLink/AppLink' export const ModalInitial = () => { - const { getModalOpened, handleClose, handleOpen } = useModalSearchParams() + const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL) - const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.INITIAL) + const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL) const [showAdvancedContent, setShowAdvancedContent] = useState(false) @@ -28,7 +28,7 @@ export const ModalInitial = () => { return ( - + diff --git a/src/components/Modal/ModalLogin/ModalLogin.tsx b/src/components/Modal/ModalLogin/ModalLogin.tsx index 01a690b..a10c578 100644 --- a/src/components/Modal/ModalLogin/ModalLogin.tsx +++ b/src/components/Modal/ModalLogin/ModalLogin.tsx @@ -14,9 +14,9 @@ import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined' import { useNavigate } from 'react-router-dom' export const ModalLogin = () => { - const { getModalOpened, handleClose } = useModalSearchParams() + const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.LOGIN) - const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.LOGIN) + const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN) const notify = useEnqueueSnackbar() @@ -37,8 +37,12 @@ export const ModalLogin = () => { const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState) + const isFormValid = + enteredUsername.trim().length > 0 && enteredPassword.trim().length > 0 + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + if (!isFormValid) return undefined try { const [username, domain] = enteredUsername.split('@') const response = await fetch( @@ -63,12 +67,7 @@ export const ModalLogin = () => { } return ( - + { )} } - type={isPasswordShown ? 'password' : 'text'} + type={isPasswordShown ? 'text' : 'password'} /> - diff --git a/src/components/Modal/ModalSettings/ModalSettings.tsx b/src/components/Modal/ModalSettings/ModalSettings.tsx index 8f87c95..981b771 100644 --- a/src/components/Modal/ModalSettings/ModalSettings.tsx +++ b/src/components/Modal/ModalSettings/ModalSettings.tsx @@ -2,7 +2,13 @@ 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, IconButton, Stack, Typography } from '@mui/material' +import { + Box, + CircularProgress, + IconButton, + Stack, + Typography, +} from '@mui/material' import { StyledButton, StyledSettingContainer, @@ -13,28 +19,37 @@ 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, useState } from 'react' +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' -export const ModalSettings = () => { - const { getModalOpened, handleClose } = useModalSearchParams() +type ModalSettingsProps = { + isSynced: boolean +} + +export const ModalSettings: FC = ({ isSynced }) => { + const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const { npub = '' } = useParams<{ npub: string }>() const notify = useEnqueueSnackbar() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS) - const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SETTINGS) + const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS) const [enteredPassword, setEnteredPassword] = useState('') const [isPasswordShown, setIsPasswordShown] = useState(false) const [isPasswordInvalid, setIsPasswordInvalid] = useState(false) - const [isPasswordSynched, setIsPasswordSynched] = useState(false) const [isChecked, setIsChecked] = useState(false) + const [isLoading, setIsLoading] = useState(false) + + + useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced]) + const handlePasswordChange = (e: ChangeEvent) => { setIsPasswordInvalid(false) setEnteredPassword(e.target.value) @@ -47,7 +62,6 @@ export const ModalSettings = () => { handleCloseModal() setEnteredPassword('') setIsPasswordInvalid(false) - setIsPasswordSynched(false) } const handleChangeCheckbox = (e: unknown, checked: boolean) => { @@ -57,19 +71,21 @@ export const ModalSettings = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setIsPasswordInvalid(false) - setIsPasswordSynched(false) if (enteredPassword.trim().length < 6) { return setIsPasswordInvalid(true) } try { + setIsLoading(true) await swicCall('saveKey', npub, enteredPassword) notify('Key saved', 'success') + dbi.addSynced(npub) // Sync npub + setEnteredPassword('') setIsPasswordInvalid(false) - setIsPasswordSynched(true) + setIsLoading(false) } catch (error) { setIsPasswordInvalid(false) - setIsPasswordSynched(false) + setIsLoading(false) } } @@ -79,7 +95,7 @@ export const ModalSettings = () => { Cloud sync - {isPasswordSynched && ( + {isSynced && ( Synched @@ -91,7 +107,7 @@ export const ModalSettings = () => { checked={isChecked} /> - Use this login on multiple devices + Use this key on multiple devices { type={isPasswordShown ? 'text' : 'password'} onChange={handlePasswordChange} value={enteredPassword} - helperText={isPasswordInvalid ? 'Invalid password' : ''} + helperText={ + isPasswordInvalid ? 'Invalid password' : '' + } placeholder='Enter a password' helperTextProps={{ sx: { @@ -122,8 +140,27 @@ export const ModalSettings = () => { }} disabled={!isChecked} /> - - Sync + {isSynced ? ( + + To change your password, type a new one and sync. + + ) : ( + + This key will be encrypted and stored on our server. You can use the password to download this key onto another device. + + )} + + Sync{' '} + {isLoading && ( + + )} diff --git a/src/components/Modal/ModalSettings/styled.tsx b/src/components/Modal/ModalSettings/styled.tsx index 0bcf339..c26f0f4 100644 --- a/src/components/Modal/ModalSettings/styled.tsx +++ b/src/components/Modal/ModalSettings/styled.tsx @@ -8,9 +8,9 @@ import { } from '@mui/material' export const StyledSettingContainer = styled((props: StackProps) => ( - + ))(({ theme }) => ({ - padding: '0.75rem', + padding: '1rem', borderRadius: '1rem', background: theme.palette.background.default, color: theme.palette.text.primary, @@ -22,6 +22,9 @@ export const StyledButton = styled(Button)(({ theme }) => { background: theme.palette.secondary.main, color: theme.palette.text.primary, }, + ':disabled': { + cursor: 'not-allowed', + }, } }) diff --git a/src/components/Modal/ModalSignUp/ModalSignUp.tsx b/src/components/Modal/ModalSignUp/ModalSignUp.tsx index 629f27d..1a4785b 100644 --- a/src/components/Modal/ModalSignUp/ModalSignUp.tsx +++ b/src/components/Modal/ModalSignUp/ModalSignUp.tsx @@ -12,9 +12,9 @@ import { swicCall } from '@/modules/swic' import { useNavigate } from 'react-router-dom' export const ModalSignUp = () => { - const { getModalOpened, handleClose } = useModalSearchParams() + const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP) - const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SIGN_UP) + const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SIGN_UP) const notify = useEnqueueSnackbar() const theme = useTheme() diff --git a/src/hooks/useIsIOS.ts b/src/hooks/useIsIOS.ts new file mode 100644 index 0000000..66f7349 --- /dev/null +++ b/src/hooks/useIsIOS.ts @@ -0,0 +1,23 @@ +import { useState, useEffect } from 'react' + +/** + * Custom hook to detect if the platform is iOS or not. + * @returns {boolean} True if the platform is iOS, false otherwise. + */ + +const iOSRegex = /iPad|iPhone|iPod/ + +function useIsIOS() { + const [isIOS, setIsIOS] = useState(false) + + useEffect(() => { + const isIOSUserAgent = + iOSRegex.test(navigator.userAgent) || + (navigator.userAgent.includes('Mac') && 'ontouchend' in document) + setIsIOS(isIOSUserAgent) + }, []) + + return isIOS +} + +export default useIsIOS diff --git a/src/hooks/useModalSearchParams.ts b/src/hooks/useModalSearchParams.ts index e713a16..6e60c87 100644 --- a/src/hooks/useModalSearchParams.ts +++ b/src/hooks/useModalSearchParams.ts @@ -17,6 +17,11 @@ export type IExtraOptions = { append?: boolean } +export type IExtraCloseOptions = { + replace?: boolean + onClose?: (s: URLSearchParams) => void +} + export const useModalSearchParams = () => { const [searchParams, setSearchParams] = useSearchParams() @@ -29,13 +34,20 @@ export const useModalSearchParams = () => { ] }, []) - const handleClose = - (modal: MODAL_PARAMS_KEYS, onClose?: (s: URLSearchParams) => void) => + const createHandleClose = + (modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) => () => { const enumKey = getEnumParam(modal) searchParams.delete(enumKey) - onClose && onClose(searchParams) - setSearchParams(searchParams) + extraOptions?.onClose && extraOptions?.onClose(searchParams) + console.log({ searchParams }) + setSearchParams(searchParams, { replace: !!extraOptions?.replace }) + } + + const createHandleCloseReplace = + (modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) => + () => { + return createHandleClose(modal, { ...extraOptions, replace: true }) } const handleOpen = useCallback( @@ -61,7 +73,7 @@ export const useModalSearchParams = () => { pathname: location.pathname, search: searchString, }, - { replace: extraOptions?.replace || true }, + { replace: !!extraOptions?.replace }, ) }, [location, navigate, getEnumParam], @@ -78,7 +90,8 @@ export const useModalSearchParams = () => { return { getModalOpened, - handleClose, + createHandleClose, + createHandleCloseReplace, handleOpen, } } diff --git a/src/hooks/useToggleConfirm.ts b/src/hooks/useToggleConfirm.ts new file mode 100644 index 0000000..78c68f9 --- /dev/null +++ b/src/hooks/useToggleConfirm.ts @@ -0,0 +1,15 @@ +import { useCallback, useState } from 'react' + +export const useToggleConfirm = () => { + const [showConfirm, setShowConfirm] = useState(false) + + const handleShow = useCallback(() => setShowConfirm(true), []) + + const handleClose = useCallback(() => setShowConfirm(false), []) + + return { + open: showConfirm, + handleShow, + handleClose, + } +} diff --git a/src/modules/backend.ts b/src/modules/backend.ts index d68481f..32b3adb 100644 --- a/src/modules/backend.ts +++ b/src/modules/backend.ts @@ -1,5 +1,5 @@ import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools' -import { dbi, DbKey, DbPending, DbPerm } from './db' +import { DbApp, dbi, DbKey, DbPending, DbPerm } from './db' import { Keys } from './keys' import NDK, { IEventHandlingStrategy, @@ -10,7 +10,7 @@ import NDK, { } from '@nostr-dev-kit/ndk' import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS } from '../utils/consts' import { Nip04 } from './nip04' -import { getReqPerm, isPackagePerm } from '@/utils/helpers/helpers' +import { getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers' //import { PrivateKeySigner } from './signer' //const PERF_TEST = false @@ -32,6 +32,7 @@ interface Key { interface Pending { req: DbPending cb: (allow: boolean, remember: boolean, options?: any) => void + notified?: boolean } interface IAllowCallbackParams { @@ -145,6 +146,7 @@ export class NoauthBackend { private enckeys: DbKey[] = [] private keys: Key[] = [] private perms: DbPerm[] = [] + private apps: DbApp[] = [] private doneReqIds: string[] = [] private confirmBuffer: Pending[] = [] private accessBuffer: DbPending[] = [] @@ -193,16 +195,25 @@ export class NoauthBackend { .matchAll({ type: 'window' }) .then((clientList) => { console.log('clients', clientList.length) + // FIXME find a client that has our + // key page for (const client of clientList) { console.log('client', client.url) if ( new URL(client.url).pathname === '/' && 'focus' in client - ) - return client.focus() + ) { + client.focus() + return + } } - // if (self.swg.clients.openWindow) - // return self.swg.clients.openWindow("/"); + + // confirm screen url + const req = event.notification.data.req + console.log("req", req) + // const url = `${self.swg.location.origin}/key/${req.npub}?confirm-connect=true&appNpub=${req.appNpub}&reqId=${req.id}` + const url = `${self.swg.location.origin}/key/${req.npub}` + self.swg.clients.openWindow(url) }), ) } @@ -216,6 +227,8 @@ export class NoauthBackend { console.log('started encKeys', this.listKeys()) this.perms = await dbi.listPerms() console.log('started perms', this.perms) + this.apps = await dbi.listApps() + console.log('started apps', this.apps) const sub = await this.swg.registration.pushManager.getSubscription() @@ -381,21 +394,69 @@ export class NoauthBackend { // and update the notifications for (const r of this.confirmBuffer) { - const text = `Confirm "${r.req.method}" by "${r.req.appNpub}"` - this.swg.registration.showNotification('Signer access', { - body: text, - tag: 'confirm-' + r.req.appNpub, - actions: [ - { - action: 'allow:' + r.req.id, - title: 'Yes', - }, - { - action: 'disallow:' + r.req.id, - title: 'No', - }, - ], - }) + + if (r.notified) continue + + const key = this.keys.find(k => k.npub === r.req.npub) + if (!key) continue + + const app = this.apps.find(a => a.appNpub === r.req.appNpub) + if (r.req.method !== 'connect' && !app) continue + + // FIXME use Nsec.app icon! + const icon = 'https://nostr.band/android-chrome-192x192.png' + + const appName = app?.name || getShortenNpub(r.req.appNpub) + // FIXME load profile? + const keyName = getShortenNpub(r.req.npub) + + const tag = 'confirm-' + r.req.appNpub + const allowAction = 'allow:' + r.req.id + const disallowAction = 'disallow:' + r.req.id + const data = { req: r.req } + + if (r.req.method === 'connect') { + const title = `Connect with new app` + const body = `Allow app "${appName}" to connect to key "${keyName}"` + this.swg.registration.showNotification(title, { + body, + tag, + icon, + data, + actions: [ + { + action: allowAction, + title: 'Connect', + }, + { + action: disallowAction, + title: 'Ignore', + }, + ], + }) + } else { + const title = `Permission request` + const body = `Allow "${r.req.method}" by "${appName}" to "${keyName}"` + this.swg.registration.showNotification(title, { + body, + tag, + icon, + data, + actions: [ + { + action: allowAction, + title: 'Yes', + }, + { + action: disallowAction, + title: 'No', + }, + ], + }) + } + + // mark + r.notified = true } if (this.notifCallback) this.notifCallback() @@ -509,6 +570,9 @@ export class NoauthBackend { icon: '', url: '', }) + + // reload + self.apps = await dbi.listApps() } } } else { @@ -771,6 +835,7 @@ export class NoauthBackend { } private async deleteApp(appNpub: string) { + this.apps = this.apps.filter((a) => a.appNpub !== appNpub) this.perms = this.perms.filter((p) => p.appNpub !== appNpub) await dbi.removeApp(appNpub) await dbi.removeAppPerms(appNpub) diff --git a/src/modules/db.ts b/src/modules/db.ts index 9a58556..b3c8bb8 100644 --- a/src/modules/db.ts +++ b/src/modules/db.ts @@ -48,23 +48,29 @@ export interface DbHistory { allowed: boolean } +export interface DbSyncHistory { + npub: string +} + export interface DbSchema extends Dexie { keys: Dexie.Table apps: Dexie.Table perms: Dexie.Table pending: Dexie.Table history: Dexie.Table + syncHistory: Dexie.Table } export const db = new Dexie('noauthdb') as DbSchema -db.version(7).stores({ +db.version(8).stores({ keys: 'npub', apps: 'appNpub,npub,name,timestamp', perms: 'id,npub,appNpub,perm,value,timestamp', pending: 'id,npub,appNpub,timestamp,method', history: 'id,npub,appNpub,timestamp,method,allowed', requestHistory: 'id', + syncHistory: 'npub', }) export const dbi = { @@ -201,4 +207,12 @@ export const dbi = { return false } }, + addSynced: async (npub: string) => { + try { + await db.syncHistory.add({ npub }) + } catch (error) { + console.log(`db addSynced error: ${error}`) + return false + } + }, } diff --git a/src/pages/AppPage/App.Page.tsx b/src/pages/AppPage/App.Page.tsx index 7d1af8a..d6c0153 100644 --- a/src/pages/AppPage/App.Page.tsx +++ b/src/pages/AppPage/App.Page.tsx @@ -1,40 +1,42 @@ -import { useLiveQuery } from 'dexie-react-hooks' -import { DbHistory, db } from '@/modules/db' import { useParams } from 'react-router' import { useAppSelector } from '@/store/hooks/redux' import { selectAppByAppNpub, selectPermsByNpubAndAppNpub } from '@/store' -import { Navigate } from 'react-router-dom' +import { Navigate, useNavigate } from 'react-router-dom' import { formatTimestampDate } from '@/utils/helpers/date' -import { Avatar, Box, Stack, Typography } from '@mui/material' +import { Box, Stack, Typography } from '@mui/material' import { SectionTitle } from '@/shared/SectionTitle/SectionTitle' import { getShortenNpub } from '@/utils/helpers/helpers' -import { PermissionMenuButton } from './styled' -import { PermissionsMenu } from './components/PermissionsMenu' -import { useOpenMenu } from '@/hooks/useOpenMenu' -import { ActivityList } from './components/ActivityList' - -const getAppHistoryQuery = (appNpub: string) => - db.history.where('appNpub').equals(appNpub).toArray() +import { Button } from '@/shared/Button/Button' +import { ACTION_TYPE } from '@/utils/consts' +import { Permissions } from './components/Permissions/Permissions' +import { StyledAppIcon } from './styled' +import { useToggleConfirm } from '@/hooks/useToggleConfirm' +import { ConfirmModal } from '@/shared/ConfirmModal/ConfirmModal' +import { swicCall } from '@/modules/swic' +import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' +import { IOSBackButton } from '@/shared/IOSBackButton/IOSBackButton' +import { ModalActivities } from './components/Activities/ModalActivities' +import { useModalSearchParams } from '@/hooks/useModalSearchParams' +import { MODAL_PARAMS_KEYS } from '@/types/modal' const AppPage = () => { const { appNpub = '', npub = '' } = useParams() + const navigate = useNavigate() + const notify = useEnqueueSnackbar() + const perms = useAppSelector((state) => selectPermsByNpubAndAppNpub(state, npub, appNpub), ) const currentApp = useAppSelector((state) => selectAppByAppNpub(state, appNpub), ) - const history = useLiveQuery( - () => { - if (!appNpub.trim().length) return [] - return getAppHistoryQuery(appNpub) - }, - [], - [] as DbHistory[], - ) - const { anchorEl, handleClose, handleOpen, open } = useOpenMenu() - const connectPerm = perms.find((perm) => perm.perm === 'connect') + const { open, handleClose, handleShow } = useToggleConfirm() + const { handleOpen: handleOpenModal } = useModalSearchParams() + + const connectPerm = perms.find( + (perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC, + ) if (!currentApp) { return @@ -43,52 +45,77 @@ const AppPage = () => { const { icon = '', name = '' } = currentApp || {} const appName = name || getShortenNpub(appNpub) const { timestamp } = connectPerm || {} + const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected' + const handleDeleteApp = async () => { + try { + await swicCall('deleteApp', appNpub) + notify(`App: «${appName}» successfully deleted!`, 'success') + navigate(`key/${npub}`) + } catch (error: any) { + notify(error?.message || 'Failed to delete app', 'error') + } + } + return ( - + <> - - - - {appName} - - - {connectedOn} - + navigate(`key/${npub}`)} /> + + + + + {appName} + + + {connectedOn} + + + + + + Disconnect + + + + + - - Permissions - - Basic/Advanced/Custom {perms.length} - - - - - - + + + ) } diff --git a/src/pages/AppPage/components/Activities/ItemActivity.tsx b/src/pages/AppPage/components/Activities/ItemActivity.tsx new file mode 100644 index 0000000..5233d7e --- /dev/null +++ b/src/pages/AppPage/components/Activities/ItemActivity.tsx @@ -0,0 +1,45 @@ +import React, { FC } from 'react' +import { DbHistory } from '@/modules/db' +import { Box, IconButton, Typography } from '@mui/material' +import { StyledActivityItem } from './styled' +import { formatTimestampDate } from '@/utils/helpers/date' +import ClearRoundedIcon from '@mui/icons-material/ClearRounded' +import DoneRoundedIcon from '@mui/icons-material/DoneRounded' +import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded' +import { ACTIONS } from '@/utils/consts' + +type ItemActivityProps = DbHistory + +export const ItemActivity: FC = ({ + allowed, + method, + timestamp, +}) => { + return ( + + + + {ACTIONS[method] || method} + + + {formatTimestampDate(timestamp)} + + + + {allowed ? ( + + ) : ( + + )} + + + + + + ) +} diff --git a/src/pages/AppPage/components/Activities/ModalActivities.tsx b/src/pages/AppPage/components/Activities/ModalActivities.tsx new file mode 100644 index 0000000..15f61ac --- /dev/null +++ b/src/pages/AppPage/components/Activities/ModalActivities.tsx @@ -0,0 +1,39 @@ +import React, { FC } from 'react' +import { Modal } from '@/shared/Modal/Modal' +import { Box } from '@mui/material' +import { useLiveQuery } from 'dexie-react-hooks' +import { HistoryDefaultValue, getActivityHistoryQuerier } from '../../utils' +import { ItemActivity } from './ItemActivity' +import { useModalSearchParams } from '@/hooks/useModalSearchParams' +import { MODAL_PARAMS_KEYS } from '@/types/modal' + +type ModalActivitiesProps = { + appNpub: string +} + +export const ModalActivities: FC = ({ appNpub }) => { + const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() + const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.ACTIVITY) + const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.ACTIVITY) + + const history = useLiveQuery( + getActivityHistoryQuerier(appNpub), + [], + HistoryDefaultValue, + ) + + return ( + + + {history.map((item) => { + return + })} + + + ) +} diff --git a/src/pages/AppPage/components/Activities/styled.tsx b/src/pages/AppPage/components/Activities/styled.tsx new file mode 100644 index 0000000..fc24619 --- /dev/null +++ b/src/pages/AppPage/components/Activities/styled.tsx @@ -0,0 +1,12 @@ +import styled from '@emotion/styled' +import { Box, BoxProps } from '@mui/material' + +export const StyledActivityItem = styled((props: BoxProps) => ( + +))(() => ({ + display: 'flex', + gap: '0.5rem', + justifyContent: 'space-between', + alignItems: 'center', + padding: '0.25rem', +})) diff --git a/src/pages/AppPage/components/ActivityList.tsx b/src/pages/AppPage/components/ActivityList.tsx deleted file mode 100644 index cd35714..0000000 --- a/src/pages/AppPage/components/ActivityList.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { FC } from 'react' -import { DbHistory } from '@/modules/db' -import { SectionTitle } from '@/shared/SectionTitle/SectionTitle' -import { Box, IconButton, Stack, Typography } from '@mui/material' -import MoreIcon from '@mui/icons-material/MoreVert' -import { ACTIONS } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent' -import { formatTimestampDate } from '@/utils/helpers/date' -import { StyledButton } from './styled' - -type ActivityListProps = { - history: DbHistory[] -} - -export const ActivityList: FC = ({ history = [] }) => { - return ( - <> - Activity - - {history.map((h) => { - return ( - - - - {ACTIONS[h.method] || h.method} - - - {h.allowed ? 'allow' : 'disallow'} - - - - - - - {formatTimestampDate(h.timestamp)} - - - ) - })} - - - ) -} diff --git a/src/pages/AppPage/components/ItemPermissionMenu.tsx b/src/pages/AppPage/components/ItemPermissionMenu.tsx deleted file mode 100644 index 658384d..0000000 --- a/src/pages/AppPage/components/ItemPermissionMenu.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { FC } from 'react' -import { Box, Typography } from '@mui/material' -import { DbPerm } from '@/modules/db' -import { ACTIONS } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent' -import { StyledMenuItem } from './styled' -import { formatTimestampDate } from '@/utils/helpers/date' - -type ItemPermissionMenuProps = { - permission: DbPerm -} - -export const ItemPermissionMenu: FC = ({ - permission, -}) => { - const { perm, value, timestamp } = permission || {} - - return ( - - - - {ACTIONS[perm] || perm} - - - {value === '1' ? 'allow' : 'disallow'} - - - - {formatTimestampDate(timestamp)} - - - ) -} diff --git a/src/pages/AppPage/components/Permissions/ItemPermission.tsx b/src/pages/AppPage/components/Permissions/ItemPermission.tsx new file mode 100644 index 0000000..a91ac6e --- /dev/null +++ b/src/pages/AppPage/components/Permissions/ItemPermission.tsx @@ -0,0 +1,59 @@ +import { FC } from 'react' +import { Box, IconButton, Typography } from '@mui/material' +import { DbPerm } from '@/modules/db' +import { formatTimestampDate } from '@/utils/helpers/date' +import { ACTIONS } from '@/utils/consts' +import { StyledPermissionItem } from './styled' +import ClearRoundedIcon from '@mui/icons-material/ClearRounded' +import DoneRoundedIcon from '@mui/icons-material/DoneRounded' +import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded' +import { ItemPermissionMenu } from './ItemPermissionMenu' +import { useOpenMenu } from '@/hooks/useOpenMenu' + +type ItemPermissionProps = { + permission: DbPerm +} + +export const ItemPermission: FC = ({ permission }) => { + const { perm, value, timestamp, id } = permission || {} + + const { anchorEl, handleClose, handleOpen, open } = useOpenMenu() + + const isAllowed = value === '1' + + return ( + <> + + + + {ACTIONS[perm] || perm} + + + {formatTimestampDate(timestamp)} + + + + {isAllowed ? ( + + ) : ( + + )} + + + + + + + + ) +} diff --git a/src/pages/AppPage/components/Permissions/ItemPermissionMenu.tsx b/src/pages/AppPage/components/Permissions/ItemPermissionMenu.tsx new file mode 100644 index 0000000..bb880c3 --- /dev/null +++ b/src/pages/AppPage/components/Permissions/ItemPermissionMenu.tsx @@ -0,0 +1,62 @@ +import { FC, useState } from 'react' +import { Menu, MenuItem, MenuProps } from '@mui/material' +import { ConfirmModal } from '@/shared/ConfirmModal/ConfirmModal' +import { swicCall } from '@/modules/swic' +import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' + +type ItemPermissionMenuProps = { + permId: string + handleClose: () => void +} & MenuProps + +export const ItemPermissionMenu: FC = ({ + open, + anchorEl, + handleClose, + permId, +}) => { + const [showConfirm, setShowConfirm] = useState(false) + const notify = useEnqueueSnackbar() + + const handleShowConfirm = () => { + setShowConfirm(true) + handleClose() + } + const handleCloseConfirm = () => setShowConfirm(false) + + const handleDeletePerm = async () => { + try { + await swicCall('deletePerm', permId) + notify('Permission successfully deleted!', 'success') + handleCloseConfirm() + } catch (error: any) { + notify(error?.message || 'Failed to delete permission', 'error') + } + } + + return ( + <> + + + Delete permission + + + + + ) +} diff --git a/src/pages/AppPage/components/Permissions/Permissions.tsx b/src/pages/AppPage/components/Permissions/Permissions.tsx new file mode 100644 index 0000000..6226a72 --- /dev/null +++ b/src/pages/AppPage/components/Permissions/Permissions.tsx @@ -0,0 +1,28 @@ +import { FC } from 'react' +import { DbPerm } from '@/modules/db' +import { SectionTitle } from '@/shared/SectionTitle/SectionTitle' +import { Box } from '@mui/material' +import { ItemPermission } from './ItemPermission' + +type PermissionsProps = { + perms: DbPerm[] +} + +export const Permissions: FC = ({ perms }) => { + return ( + + Permissions + + {perms.map((perm) => { + return + })} + + + ) +} diff --git a/src/pages/AppPage/components/Permissions/styled.tsx b/src/pages/AppPage/components/Permissions/styled.tsx new file mode 100644 index 0000000..2acb89c --- /dev/null +++ b/src/pages/AppPage/components/Permissions/styled.tsx @@ -0,0 +1,11 @@ +import { Box, BoxProps, styled } from '@mui/material' + +export const StyledPermissionItem = styled((props: BoxProps) => ( + +))(() => ({ + display: 'flex', + gap: '0.5rem', + justifyContent: 'space-between', + alignItems: 'center', + padding: '0.5rem', +})) diff --git a/src/pages/AppPage/components/PermissionsMenu.tsx b/src/pages/AppPage/components/PermissionsMenu.tsx deleted file mode 100644 index a10db4d..0000000 --- a/src/pages/AppPage/components/PermissionsMenu.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { DbPerm } from '@/modules/db' -import { Menu, MenuItem, MenuProps } from '@mui/material' -import { FC } from 'react' -import { ItemPermissionMenu } from './ItemPermissionMenu' - -type PermissionsMenuProps = { - perms: DbPerm[] -} & MenuProps - -export const PermissionsMenu: FC = ({ - perms, - open, - anchorEl, - onClose, -}) => { - const isNoPerms = perms.length === 0 - return ( - - {isNoPerms && No permissions} - {!isNoPerms && - perms.map((perm) => ( - - ))} - - ) -} diff --git a/src/pages/AppPage/components/styled.tsx b/src/pages/AppPage/components/styled.tsx index e6343f6..36d7006 100644 --- a/src/pages/AppPage/components/styled.tsx +++ b/src/pages/AppPage/components/styled.tsx @@ -1,17 +1,5 @@ import { Button } from '@/shared/Button/Button' -import { MenuItem, MenuItemProps, styled } from '@mui/material' - -export const StyledMenuItem = styled((props: MenuItemProps) => ( - -))(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: '0.5rem', - '&:not(:first-of-type)': { - borderTop: '1px solid ' + theme.palette.secondary.main, - }, -})) +import { styled } from '@mui/material' export const StyledButton = styled(Button)({ textTransform: 'capitalize', diff --git a/src/pages/AppPage/styled.tsx b/src/pages/AppPage/styled.tsx index 65157d7..e744b70 100644 --- a/src/pages/AppPage/styled.tsx +++ b/src/pages/AppPage/styled.tsx @@ -1,6 +1,8 @@ -import { AppButtonProps, Button } from '@/shared/Button/Button' -import { styled } from '@mui/material' +import { Avatar, AvatarProps, styled } from '@mui/material' -export const PermissionMenuButton = styled((props: AppButtonProps) => ( - + + + + ) +} diff --git a/src/shared/ConfirmModal/styled.tsx b/src/shared/ConfirmModal/styled.tsx new file mode 100644 index 0000000..6cfb7e8 --- /dev/null +++ b/src/shared/ConfirmModal/styled.tsx @@ -0,0 +1,11 @@ +import { + DialogContentText, + DialogContentTextProps, + styled, +} from '@mui/material' + +export const StyledDialogContentText = styled( + (props: DialogContentTextProps) => , +)(({ theme }) => ({ + color: theme.palette.primary.main, +})) diff --git a/src/shared/IOSBackButton/IOSBackButton.tsx b/src/shared/IOSBackButton/IOSBackButton.tsx new file mode 100644 index 0000000..3a82991 --- /dev/null +++ b/src/shared/IOSBackButton/IOSBackButton.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react' +import { ButtonProps } from '@mui/material' +import { useNavigate } from 'react-router-dom' +import useIsIOS from '@/hooks/useIsIOS' +import { StyledButton } from './styled' + +type IOSBackButtonProps = ButtonProps & { + onNavigate?: () => void +} + +export const IOSBackButton: FC = ({ onNavigate }) => { + const isIOS = useIsIOS() + const navigate = useNavigate() + + const handleNavigateBack = () => { + if (onNavigate && typeof onNavigate === 'function') { + return onNavigate() + } + navigate(-1) + } + + if (!isIOS) return null + + return Back +} diff --git a/src/shared/IOSBackButton/styled.tsx b/src/shared/IOSBackButton/styled.tsx new file mode 100644 index 0000000..9cfe2f8 --- /dev/null +++ b/src/shared/IOSBackButton/styled.tsx @@ -0,0 +1,21 @@ +import { Button, ButtonProps, styled } from '@mui/material' +import GoBackIcon from '@mui/icons-material/KeyboardBackspaceRounded' + +export const StyledButton = styled((props: ButtonProps) => ( +