sigit.io/src/pages/nostr/index.tsx

189 lines
4.7 KiB
TypeScript
Raw Normal View History

import { launch as launchNostrLoginDialog } from 'nostr-login'
2024-07-29 17:12:47 +02:00
import { Button, Divider, TextField } from '@mui/material'
2024-07-26 15:45:13 +02:00
import { getPublicKey, nip19 } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { useAppDispatch } from '../../hooks/store'
2024-07-26 15:45:13 +02:00
import { useNavigate, useSearchParams } from 'react-router-dom'
import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { AuthController } from '../../controllers'
import { updateKeyPair, updateLoginMethod } from '../../store/actions'
import { LoginMethod } from '../../store/auth/types'
2024-07-26 15:45:13 +02:00
import { hexToBytes } from '@noble/hashes/utils'
2024-07-30 12:23:38 +02:00
import styles from './styles.module.scss'
2024-07-29 17:12:47 +02:00
export const Nostr = () => {
2024-07-26 15:45:13 +02:00
const [searchParams] = useSearchParams()
const dispatch = useAppDispatch()
2024-07-26 15:45:13 +02:00
const navigate = useNavigate()
const authController = new AuthController()
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [inputValue, setInputValue] = useState('')
const navigateAfterLogin = (path: string) => {
const callbackPath = searchParams.get('callbackPath')
if (callbackPath) {
// base64 decoded path
const path = atob(callbackPath)
navigate(path)
return
}
navigate(path)
}
2024-07-26 15:45:13 +02:00
const [isNostrExtensionAvailable, setIsNostrExtensionAvailable] =
useState(false)
useEffect(() => {
setTimeout(() => {
setIsNostrExtensionAvailable(!!window.nostr)
}, 500)
}, [])
/**
* Call login function when enter is pressed
*/
2024-08-05 09:49:56 +02:00
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
2024-07-26 15:45:13 +02:00
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
event.preventDefault()
login()
}
}
/**
* Login with NSEC or HEX private key
* @param privateKey in HEX format
*/
const loginWithNsec = async (privateKey?: Uint8Array) => {
let nsec = ''
if (privateKey) {
nsec = nip19.nsecEncode(privateKey)
} else {
nsec = inputValue
try {
privateKey = nip19.decode(nsec).data as Uint8Array
} catch (err) {
toast.error(`Error decoding the nsec. ${err}`)
}
}
if (!privateKey) {
toast.error(
'Snap, we failed to convert the private key you provided. Please make sure key is valid.'
)
setIsLoading(false)
return
}
const publickey = getPublicKey(privateKey)
dispatch(
updateKeyPair({
private: nsec,
public: publickey
})
)
dispatch(updateLoginMethod(LoginMethod.privateKey))
2024-07-26 15:45:13 +02:00
setIsLoading(true)
setLoadingSpinnerDesc('Authenticating and finding metadata')
const redirectPath = await authController
.authAndGetMetadataAndRelaysMap(publickey)
.catch((err) => {
toast.error('Error occurred in authentication: ' + err)
return null
})
if (redirectPath) navigateAfterLogin(redirectPath)
setIsLoading(false)
setLoadingSpinnerDesc('')
}
const login = () => {
if (inputValue.startsWith('nsec')) {
return loginWithNsec()
}
// Check if maybe hex nsec
try {
const privateKey = hexToBytes(inputValue)
const publickey = getPublicKey(privateKey)
if (publickey) return loginWithNsec(privateKey)
} catch (err) {
console.warn('err', err)
}
toast.error('Invalid format, please use: private key (hex or nsec)')
2024-07-26 15:45:13 +02:00
return
}
return (
<>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
2024-07-29 17:12:47 +02:00
{isNostrExtensionAvailable && (
<>
2024-07-30 12:23:38 +02:00
<label className={styles.label} htmlFor="extension-login">
Login by using a{' '}
<a
rel="noopener"
href="https://github.com/nostrband/nostr-login"
target="_blank"
>
nostr-login
</a>
2024-07-29 17:12:47 +02:00
</label>
<Button
id="nostr-login"
2024-07-29 17:12:47 +02:00
variant="contained"
onClick={() => {
launchNostrLoginDialog()
}}
2024-07-29 17:12:47 +02:00
>
Nostr Login
2024-07-29 17:12:47 +02:00
</Button>
<Divider
sx={{
fontSize: '16px'
}}
>
or
</Divider>
</>
)}
<form autoComplete="off">
<TextField
onKeyDown={handleInputKeyDown}
label="Private key (Not recommended)"
type="password"
autoComplete="off"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
fullWidth
margin="dense"
/>
</form>
2024-07-29 17:12:47 +02:00
<Button
disabled={!inputValue}
onClick={login}
variant="contained"
fullWidth
>
Login
</Button>
2024-07-26 15:45:13 +02:00
</>
)
}