Implement connectApp logic, add app url and icon

This commit is contained in:
artur 2024-02-08 14:15:45 +03:00
parent caf8f9a82b
commit 48c07ad1c0
11 changed files with 350 additions and 90 deletions

View File

@ -1,9 +1,9 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { call, getAppIconTitle, getShortenNpub } from '@/utils/helpers/helpers'
import { call, getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
import { Avatar, Box, Stack, Typography } from '@mui/material'
import { useParams, useSearchParams } from 'react-router-dom'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks/redux'
import { selectAppsByNpub } from '@/store'
import { StyledButton, StyledToggleButtonsGroup } from './styled'
@ -11,25 +11,33 @@ import { ActionToggleButton } from './сomponents/ActionToggleButton'
import { useState } from 'react'
import { swicCall } from '@/modules/swic'
import { ACTION_TYPE } from '@/utils/consts'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
export const ModalConfirmConnect = () => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
const notify = useEnqueueSnackbar()
const navigate = useNavigate()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
const { npub = '' } = useParams<{ npub: string }>()
const [searchParams] = useSearchParams()
const paramNpub = searchParams.get('npub') || ''
const { npub = paramNpub } = useParams<{ npub: string }>()
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC)
const [searchParams] = useSearchParams()
const appNpub = searchParams.get('appNpub') || ''
const pendingReqId = searchParams.get('reqId') || ''
const isPopup = searchParams.get('popup') === 'true'
const token = searchParams.get('token') || ''
const triggerApp = apps.find((app) => app.appNpub === appNpub)
const { name, icon = '' } = triggerApp || {}
const appName = name || getShortenNpub(appNpub)
const appAvatarTitle = getAppIconTitle(name, appNpub)
const { name, url = '', icon = '' } = triggerApp || {}
const appUrl = url || searchParams.get('appUrl') || ''
const appDomain = getDomain(appUrl)
const appName = name || appDomain || getShortenNpub(appNpub)
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
@ -47,6 +55,9 @@ export const ModalConfirmConnect = () => {
onClose: (sp) => {
sp.delete('appNpub')
sp.delete('reqId')
sp.delete('popup')
sp.delete('npub')
sp.delete('appUrl')
},
})
@ -59,16 +70,57 @@ export const ModalConfirmConnect = () => {
if (isPopup) window.close()
}
const allow = () => {
const options: any = {}
if (selectedActionType === ACTION_TYPE.BASIC) options.perms = [ACTION_TYPE.BASIC]
// else
// options.perms = ['connect','get_public_key'];
confirmPending(pendingReqId, true, true, options)
const allow = async () => {
let perms = ['connect','get_public_key'];
if (selectedActionType === ACTION_TYPE.BASIC) perms = [ACTION_TYPE.BASIC]
if (pendingReqId) {
const options = { perms }
await confirmPending(pendingReqId, true, true, options)
} else {
try {
await swicCall('enablePush')
console.log('enablePush done')
} catch (e: any) {
console.log('error', e)
notify('Please enable Notifications in website settings!', 'error')
return;
}
try {
await swicCall('connectApp', { npub, appNpub, appUrl, perms })
console.log('connectApp done', npub, appNpub, appUrl, perms)
} catch (e: any) {
notify(e.toString(), 'error')
return;
}
if (token) {
try {
await swicCall('redeemToken', npub, token)
console.log('redeemToken done')
} catch (e) {
console.log("error", e);
notify('App did not reply. Please try to log in now.', 'error')
navigate(`/key/${npub}`, { replace: true })
return;
}
}
notify('App connected! Closing...', 'success')
if (isPopup)
setTimeout(() => window.close(), 3000);
else
navigate(`/key/${npub}`, { replace: true })
}
}
const disallow = () => {
confirmPending(pendingReqId, false, true)
if (pendingReqId)
confirmPending(pendingReqId, false, true)
else
closeModalAfterRequest()
}
if (isPopup) {
@ -91,7 +143,7 @@ export const ModalConfirmConnect = () => {
width: 56,
height: 56,
}}
src={icon}
src={appIcon}
>
{appAvatarTitle}
</Avatar>

View File

@ -1,7 +1,7 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { call, getAppIconTitle, getReqActionName, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers'
import { call, getAppIconTitle, getReqActionName, getShortenNpub } from '@/utils/helpers/helpers'
import { Avatar, Box, List, ListItem, ListItemIcon, ListItemText, Stack, Typography } from '@mui/material'
import { useParams, useSearchParams } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks/redux'

View File

@ -246,12 +246,12 @@ export class NoauthBackend {
return Buffer.from(await this.swg.crypto.subtle.digest('SHA-256', Buffer.from(s))).toString('hex')
}
private async fetchNpubName(npub: string) {
private async fetchNpubName(npub: string) {
const url = `${NOAUTHD_URL}/name?npub=${npub}`
const r = await fetch(url)
const d = await r.json()
return d?.names?.length ? d.names[0] as string : ''
}
const d = await r.json()
return d?.names?.length ? (d.names[0] as string) : ''
}
private async sendPost({ url, method, headers, body }: { url: string; method: string; headers: any; body: string }) {
const r = await fetch(url, {
@ -407,6 +407,23 @@ export class NoauthBackend {
throw new Error('Too many requests, retry later')
}
private async sendTokenToServer(npub: string, token: string) {
const body = JSON.stringify({
npub,
token,
})
const method = 'POST'
const url = `${NOAUTHD_URL}/created`
return this.sendPostAuthd({
npub,
url,
method,
body,
})
}
private notify() {
// FIXME collect info from accessBuffer and confirmBuffer
// and update the notifications
@ -550,6 +567,50 @@ export class NoauthBackend {
return perm?.value || ''
}
private async connectApp({
npub,
appNpub,
appUrl,
perms,
appName = '',
appIcon = ''
}: {
npub: string,
appNpub: string,
appUrl: string,
appName?: string,
appIcon?: string,
perms: string[]
}) {
await dbi.addApp({
appNpub: appNpub,
npub: npub,
timestamp: Date.now(),
name: appName,
icon: appIcon,
url: appUrl,
})
// reload
this.apps = await dbi.listApps()
// write new perms confirmed by user
for (const p of perms) {
await dbi.addPerm({
id: Math.random().toString(36).substring(7),
npub: npub,
appNpub: appNpub,
perm: p,
value: '1',
timestamp: Date.now(),
})
}
// reload
this.perms = await dbi.listPerms()
}
private async allowPermitCallback({
backend,
npub,
@ -566,13 +627,13 @@ export class NoauthBackend {
}
const appNpub = nip19.npubEncode(remotePubkey)
const connected = !!this.apps.find(a => a.appNpub === appNpub)
if (!connected && method !== 'connect') {
const connected = !!this.apps.find((a) => a.appNpub === appNpub)
if (!connected && method !== 'connect') {
console.log('ignoring request before connect', method, id, appNpub, npub)
return false
}
}
const req: DbPending = {
const req: DbPending = {
id,
npub,
appNpub,
@ -588,24 +649,24 @@ export class NoauthBackend {
// confirm
console.log(Date.now(), allow ? 'allowed' : 'disallowed', npub, method, options, params)
if (manual) {
if (manual) {
await dbi.confirmPending(id, allow)
// add app on 'allow connect'
// add app on 'allow connect'
if (method === 'connect' && allow) {
// if (!(await dbi.getApp(req.appNpub))) {
await dbi.addApp({
appNpub: req.appNpub,
npub: req.npub,
timestamp: Date.now(),
name: '',
icon: '',
url: '',
})
await dbi.addApp({
appNpub: req.appNpub,
npub: req.npub,
timestamp: Date.now(),
name: '',
icon: '',
url: '',
})
// reload
self.apps = await dbi.listApps()
}
// reload
self.apps = await dbi.listApps()
}
} else {
// just send to db w/o waiting for it
dbi.addConfirmed({
@ -625,7 +686,7 @@ export class NoauthBackend {
let newPerms = [getReqPerm(req)]
if (allow && options && options.perms) newPerms = options.perms
// write new perms confirmed by user
// write new perms confirmed by user
for (const p of newPerms) {
await dbi.addPerm({
id: req.id,
@ -635,14 +696,14 @@ export class NoauthBackend {
value: allow ? '1' : '0',
timestamp: Date.now(),
})
}
}
// reload
// reload
this.perms = await dbi.listPerms()
// confirm pending requests that might now have
// the proper perms
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
// confirm pending requests that might now have
// the proper perms
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected)
for (const r of otherReqs) {
let perm = this.getPerm(r.req)
@ -790,6 +851,11 @@ export class NoauthBackend {
return k
}
private async redeemToken(npub: string, token: string) {
console.log('redeeming token', npub, token)
await this.sendTokenToServer(npub, token)
}
private async importKey(name: string, nsec: string) {
const k = await this.addKey({ name, nsec })
this.updateUI()
@ -821,42 +887,42 @@ export class NoauthBackend {
const key = this.enckeys.find((k) => k.npub === npub)
if (key) return this.keyInfo(key)
let name = ''
let existingName = true
// check name - user might have provided external nip05,
// or just his npub - we must fetch their name from our
// server, and if not exists - try to assign one
const npubName = await this.fetchNpubName(npub)
if (npubName) {
// already have name for this npub
console.log("existing npub name", npub, npubName)
name = npubName
} else if (nip05.includes('@')) {
// no name for them?
const [nip05name, domain] = nip05.split('@')
if (domain === DOMAIN) {
// wtf? how did we learn their npub if
// it's the name on our server but we can't fetch it?
console.log("existing name", nip05name)
name = nip05name
} else {
// try to take same name on our domain
existingName = false
name = nip05name
let takenName = await fetchNip05(`${name}@${DOMAIN}`)
if (takenName) {
// already taken? try name_domain as name
name = `${nip05name}_${domain}`
takenName = await fetchNip05(`${name}@${DOMAIN}`)
}
if (takenName) {
console.log("All names taken, leave without a name?")
name = ''
}
}
}
let name = ''
let existingName = true
// check name - user might have provided external nip05,
// or just his npub - we must fetch their name from our
// server, and if not exists - try to assign one
const npubName = await this.fetchNpubName(npub)
if (npubName) {
// already have name for this npub
console.log('existing npub name', npub, npubName)
name = npubName
} else if (nip05.includes('@')) {
// no name for them?
const [nip05name, domain] = nip05.split('@')
if (domain === DOMAIN) {
// wtf? how did we learn their npub if
// it's the name on our server but we can't fetch it?
console.log('existing name', nip05name)
name = nip05name
} else {
// try to take same name on our domain
existingName = false
name = nip05name
let takenName = await fetchNip05(`${name}@${DOMAIN}`)
if (takenName) {
// already taken? try name_domain as name
name = `${nip05name}_${domain}`
takenName = await fetchNip05(`${name}@${DOMAIN}`)
}
if (takenName) {
console.log('All names taken, leave without a name?')
name = ''
}
}
}
console.log("fetch", { name, existingName })
console.log('fetch', { name, existingName })
// add new key
const nsec = await this.keysModule.decryptKeyPass({
@ -926,6 +992,8 @@ export class NoauthBackend {
let result = undefined
if (method === 'generateKey') {
result = await this.generateKey(args[0])
} else if (method === 'redeemToken') {
result = await this.redeemToken(args[0], args[1])
} else if (method === 'importKey') {
result = await this.importKey(args[0], args[1])
} else if (method === 'saveKey') {
@ -934,6 +1002,8 @@ export class NoauthBackend {
result = await this.fetchKey(args[0], args[1], args[2])
} else if (method === 'confirm') {
result = await this.confirm(args[0], args[1], args[2], args[3])
} else if (method === 'connectApp') {
result = await this.connectApp(args[0])
} else if (method === 'deleteApp') {
result = await this.deleteApp(args[0])
} else if (method === 'deletePerm') {

View File

@ -15,7 +15,7 @@ type ItemPermissionProps = {
}
export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
const { perm, value, timestamp, id } = permission || {}
const { value, timestamp, id } = permission || {}
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()

View File

@ -0,0 +1,100 @@
import { Stack, Typography } from '@mui/material'
import { GetStartedButton, LearnMoreButton } from './styled'
import { DOMAIN } from '@/utils/consts'
import { useSearchParams } from 'react-router-dom'
import { swicCall } from '@/modules/swic'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { ModalConfirmConnect } from '@/components/Modal/ModalConfirmConnect/ModalConfirmConnect'
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
const CreatePage = () => {
const notify = useEnqueueSnackbar()
const { handleOpen } = useModalSearchParams()
const [searchParams] = useSearchParams()
const name = searchParams.get('name') || ''
const token = searchParams.get('token') || ''
const appNpub = searchParams.get('appNpub') || ''
const isValid = name && token && appNpub
const nip05 = `${name}@${DOMAIN}`
const handleLearnMore = () => {
// @ts-ignore
window.open(`https://${DOMAIN}`, '_blank').focus()
}
const handleClickAddAccount = async () => {
try {
const key: any = await swicCall('generateKey', name)
let appUrl = ''
if (window.document.referrer) {
try {
const u = new URL(window.document.referrer)
appUrl = u.origin
} catch {}
}
console.log('Created', key.npub, 'app', appUrl)
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
search: {
npub: key.npub,
appNpub,
appUrl,
token,
// will close after all done
popup: 'true'
},
});
} catch (error: any) {
notify(error.message || error.toString(), 'error')
}
}
if (!isValid) {
return (
<Stack maxHeight={'100%'} overflow={'auto'}>
<Typography textAlign={'center'} variant="h6" paddingTop="1em">
Bad parameters.
</Typography>
</Stack>
)
}
return (
<>
<Stack maxHeight={'100%'} overflow={'auto'}>
<Typography textAlign={'center'} variant="h4" paddingTop="0.5em">
Welcome to Nostr!
</Typography>
<Stack gap={'0.5rem'} overflow={'auto'}>
<Typography textAlign={'left'} variant="h6" paddingTop="0.5em">
Chosen name: <b>{nip05}</b>
</Typography>
<GetStartedButton onClick={handleClickAddAccount}>Create account</GetStartedButton>
<Typography textAlign={'left'} variant="h5" paddingTop="1em">
What you need to know:
</Typography>
<ol style={{ marginLeft: '1em' }}>
<li>Nostr accounts are based on cryptographic keys.</li>
<li>All your actions on Nostr will be signed by your keys.</li>
<li>Nsec.app is one of many services to manage Nostr keys.</li>
<li>When you create an account, a new key will be created.</li>
<li>This key can later be used with other Nostr websites.</li>
</ol>
<LearnMoreButton onClick={handleLearnMore}>Learn more</LearnMoreButton>
</Stack>
</Stack>
<ModalConfirmConnect />
</>
)
}
export default CreatePage

View File

@ -0,0 +1,26 @@
import { AppButtonProps, Button } from '@/shared/Button/Button'
import { styled } from '@mui/material'
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined'
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'
export const AddAccountButton = styled((props: AppButtonProps) => (
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
))(() => ({
alignSelf: 'center',
padding: '0.35rem 1rem',
}))
export const GetStartedButton = styled((props: AppButtonProps) => (
<Button {...props} startIcon={<PlayArrowOutlinedIcon />} />
))(() => ({
alignSelf: 'left',
padding: '0.35rem 1rem',
}))
export const LearnMoreButton = styled((props: AppButtonProps) => (
<Button {...props} startIcon={<HelpOutlineOutlinedIcon />} />
))(() => ({
alignSelf: 'left',
padding: '0.35rem 1rem',
}))

View File

@ -18,7 +18,7 @@ const HomePage = () => {
const handleLearnMore = () => {
// @ts-ignore
window.open(`https://info.${DOMAIN}`, '_blank').focus()
window.open(`https://${DOMAIN}`, '_blank').focus()
}
return (

View File

@ -15,7 +15,7 @@ type AppsProps = {
npub: string
}
export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
export const Apps: FC<AppsProps> = ({ apps = [] }) => {
const notify = useEnqueueSnackbar()
// eslint-disable-next-line

View File

@ -2,15 +2,16 @@ import { DbApp } from '@/modules/db'
import { Avatar, Stack, Typography } from '@mui/material'
import { FC } from 'react'
import { Link } from 'react-router-dom'
// import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
import { getAppIconTitle, getShortenNpub } from '@/utils/helpers/helpers'
import { getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
import { StyledItemAppContainer } from './styled'
type ItemAppProps = DbApp
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
const appName = name || getShortenNpub(appNpub)
const appAvatarTitle = getAppIconTitle(name, appNpub)
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) => {
const appDomain = getDomain(url)
const appName = name || appDomain || getShortenNpub(appNpub)
const appIcon = icon || `https://${appDomain}/favicon.ico`
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
return (
<StyledItemAppContainer
direction={'row'}
@ -23,8 +24,8 @@ export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
<Avatar
variant="rounded"
sx={{ width: 56, height: 56 }}
src={icon}
alt={name}
src={appIcon}
alt={appName}
>
{appAvatarTitle}
</Avatar>

View File

@ -4,6 +4,7 @@ import HomePage from '../pages/HomePage/Home.Page'
import WelcomePage from '../pages/Welcome.Page'
import { Layout } from '../layout/Layout'
import { CircularProgress, Stack } from '@mui/material'
import CreatePage from '@/pages/CreatePage/Create.Page'
const KeyPage = lazy(() => import('../pages/KeyPage/Key.Page'))
const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
@ -26,6 +27,7 @@ const AppRoutes = () => {
<Route path="/key/:npub" element={<KeyPage />} />
<Route path="/key/:npub/app/:appNpub" element={<AppPage />} />
<Route path="/key/:npub/:req_id" element={<ConfirmPage />} />
<Route path="/create" element={<CreatePage />} />
</Route>
<Route path="*" element={<Navigate to={'/home'} />} />
</Routes>

View File

@ -3,14 +3,23 @@ import { ACTIONS, ACTION_TYPE, NIP46_RELAYS } from '../consts'
import { DbPending, DbPerm } from '@/modules/db'
import { MetaEvent } from '@/types/meta-event'
export async function call(cb: () => any) {
export async function call(cb: () => any, err?: (e: string) => void) {
try {
return await cb()
} catch (e) {
} catch (e: any) {
console.log(`Error: ${e}`)
err?.(e.toString());
}
}
export const getDomain = (url: string) => {
try {
return new URL(url).hostname
} catch {
return ''
}
}
export const getShortenNpub = (npub = '') => {
return npub.substring(0, 10) + '...' + npub.slice(-4)
}