230 lines
8.0 KiB
TypeScript
230 lines
8.0 KiB
TypeScript
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||
import { Modal } from '@/shared/Modal/Modal'
|
||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||
import {
|
||
askNotificationPermission,
|
||
call,
|
||
getAppIconTitle,
|
||
getDomain,
|
||
getReferrerAppUrl,
|
||
getShortenNpub,
|
||
} from '@/utils/helpers/helpers'
|
||
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, selectPendingsByNpub } from '@/store'
|
||
import { StyledButton, StyledToggleButtonsGroup } from './styled'
|
||
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
||
import { useEffect, useState } from 'react'
|
||
import { swicCall, swicWaitStarted } from '@/modules/swic'
|
||
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()
|
||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||
|
||
const [searchParams] = useSearchParams()
|
||
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>(ACTION_TYPE.BASIC)
|
||
const [isLoaded, setIsLoaded] = useState(false)
|
||
|
||
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, url = '', icon = '' } = triggerApp || {}
|
||
|
||
const appUrl = url || searchParams.get('appUrl') || getReferrerAppUrl()
|
||
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 closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||
onClose: (sp) => {
|
||
sp.delete('appNpub')
|
||
sp.delete('reqId')
|
||
sp.delete('popup')
|
||
sp.delete('npub')
|
||
sp.delete('appUrl')
|
||
},
|
||
})
|
||
|
||
// NOTE: when opened directly to this modal using authUrl,
|
||
// we might not have pending requests visible yet bcs we haven't
|
||
// loaded them yet, which means this modal will be closed with
|
||
// the logic below. So now if it's popup then we wait for SW
|
||
// and then wait a little more to give it time to fetch
|
||
// pending reqs from db. Same logic implemented in confirm-event.
|
||
|
||
// FIXME move to a separate hook and reuse?
|
||
|
||
useEffect(() => {
|
||
if (isModalOpened) {
|
||
if (isPopup) {
|
||
console.log("waiting for sw")
|
||
// wait for SW to start
|
||
swicWaitStarted().then(() => {
|
||
// give it some time to load the pending reqs etc
|
||
console.log("waiting for sw done")
|
||
setTimeout(() => setIsLoaded(true), 500)
|
||
})
|
||
} else {
|
||
setIsLoaded(true)
|
||
}
|
||
} else {
|
||
setIsLoaded(false)
|
||
}
|
||
}, [isModalOpened, isPopup])
|
||
|
||
if (isLoaded) {
|
||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||
// NOTE: app doesn't exist yet!
|
||
// const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId)
|
||
// console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending});
|
||
if (isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
|
||
if (isPopup) window.close()
|
||
else 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)
|
||
console.log('confirmed', id, allow, remember, options)
|
||
closeModalAfterRequest()
|
||
})
|
||
if (isPopup) window.close()
|
||
}
|
||
|
||
const allow = async () => {
|
||
let perms = ['connect', 'get_public_key']
|
||
if (selectedActionType === ACTION_TYPE.BASIC) perms = [ACTION_TYPE.BASIC]
|
||
|
||
if (pendingReqId) {
|
||
const options = { perms, appUrl }
|
||
await confirmPending(pendingReqId, true, true, options)
|
||
} else {
|
||
try {
|
||
await askNotificationPermission()
|
||
const result = await swicCall('enablePush')
|
||
if (!result) throw new Error('Failed to activate the push subscription')
|
||
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 = () => {
|
||
if (pendingReqId) confirmPending(pendingReqId, false, true)
|
||
else closeModalAfterRequest()
|
||
}
|
||
|
||
if (isPopup) {
|
||
document.addEventListener('visibilitychange', () => {
|
||
if (document.visibilityState === 'hidden') {
|
||
disallow()
|
||
}
|
||
})
|
||
}
|
||
|
||
return (
|
||
<Modal title="Connection request" open={isModalOpened} withCloseButton={false}>
|
||
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
||
{!pendingReqId && (
|
||
<Typography variant="body1" color={'GrayText'}>
|
||
You will be asked to <b>enable notifications</b>, this is needed for a reliable communication with Nostr
|
||
apps.
|
||
</Typography>
|
||
)}
|
||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
||
<Avatar
|
||
variant="rounded"
|
||
sx={{
|
||
width: 56,
|
||
height: 56,
|
||
}}
|
||
src={appIcon}
|
||
>
|
||
{appAvatarTitle}
|
||
</Avatar>
|
||
<Box>
|
||
<Typography variant="h5" fontWeight={600}>
|
||
{appName}
|
||
</Typography>
|
||
<Typography variant="body2" color={'GrayText'}>
|
||
New app would like to connect
|
||
</Typography>
|
||
</Box>
|
||
</Stack>
|
||
<StyledToggleButtonsGroup value={selectedActionType} onChange={handleActionTypeChange} exclusive>
|
||
<ActionToggleButton
|
||
value={ACTION_TYPE.BASIC}
|
||
title="Basic permissions"
|
||
description="Read your public key, sign notes, reactions, zaps, etc"
|
||
/>
|
||
<ActionToggleButton
|
||
value={ACTION_TYPE.CUSTOM}
|
||
title="On demand"
|
||
description="Confirm permissions when the app asks for them"
|
||
/>
|
||
</StyledToggleButtonsGroup>
|
||
<Stack direction={'row'} gap={'1rem'}>
|
||
<StyledButton onClick={disallow} varianttype="secondary">
|
||
Ignore
|
||
</StyledButton>
|
||
<StyledButton fullWidth onClick={allow}>
|
||
Connect
|
||
</StyledButton>
|
||
</Stack>
|
||
</Stack>
|
||
</Modal>
|
||
)
|
||
}
|