2023-08-08 00:50:01 +00:00
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
import { gql, useMutation } from '@apollo/client'
|
|
|
|
import { signIn } from 'next-auth/react'
|
|
|
|
import Container from 'react-bootstrap/Container'
|
|
|
|
import Col from 'react-bootstrap/Col'
|
|
|
|
import Row from 'react-bootstrap/Row'
|
|
|
|
import { useRouter } from 'next/router'
|
|
|
|
import AccordianItem from './accordian-item'
|
|
|
|
import BackIcon from '../svgs/arrow-left-line.svg'
|
|
|
|
import styles from './lightning-auth.module.css'
|
2023-12-19 22:01:48 +00:00
|
|
|
import { callWithTimeout } from '../lib/nostr'
|
2023-08-08 00:50:01 +00:00
|
|
|
|
|
|
|
function ExtensionError ({ message, details }) {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<h4 className='fw-bold text-danger pb-1'>error: {message}</h4>
|
|
|
|
<div className='text-muted pb-4'>{details}</div>
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function NostrExplainer ({ text }) {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<ExtensionError message='nostr extension not found' details='Nostr extensions are the safest way to use your nostr identity on Stacker News.' />
|
|
|
|
<Row className='w-100 text-muted'>
|
|
|
|
<AccordianItem
|
|
|
|
header={`Which extensions can I use to ${(text || 'Login').toLowerCase()} with Nostr?`}
|
|
|
|
show
|
|
|
|
body={
|
|
|
|
<>
|
|
|
|
<Row>
|
|
|
|
<Col>
|
|
|
|
<ul>
|
|
|
|
<li>
|
|
|
|
<a href='https://getalby.com'>Alby</a><br />
|
|
|
|
available for: chrome, firefox, and safari
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
<a href='https://www.getflamingo.org/'>Flamingo</a><br />
|
|
|
|
available for: chrome
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
<a href='https://github.com/fiatjaf/nos2x'>nos2x</a><br />
|
|
|
|
available for: chrome
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
<a href='https://diegogurpegui.com/nos2x-fox/'>nos2x-fox</a><br />
|
|
|
|
available for: firefox
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
<a href='https://github.com/fiatjaf/horse'>horse</a><br />
|
|
|
|
available for: chrome<br />
|
|
|
|
supports hardware signing
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</Col>
|
|
|
|
</Row>
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</Row>
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function NostrAuth ({ text, callbackUrl }) {
|
|
|
|
const [createAuth, { data, error }] = useMutation(gql`
|
|
|
|
mutation createAuth {
|
|
|
|
createAuth {
|
|
|
|
k1
|
|
|
|
}
|
2023-11-12 17:59:18 +00:00
|
|
|
}`, {
|
|
|
|
// don't cache this mutation
|
|
|
|
fetchPolicy: 'no-cache'
|
|
|
|
})
|
2023-08-08 00:50:01 +00:00
|
|
|
const [hasExtension, setHasExtension] = useState(undefined)
|
|
|
|
const [extensionError, setExtensionError] = useState(null)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
createAuth()
|
2023-11-12 17:59:18 +00:00
|
|
|
setHasExtension(!!window.nostr)
|
2023-08-08 00:50:01 +00:00
|
|
|
}, [])
|
|
|
|
|
|
|
|
const k1 = data?.createAuth.k1
|
|
|
|
|
|
|
|
useEffect(() => {
|
2023-11-12 17:59:18 +00:00
|
|
|
if (!k1 || !hasExtension) return
|
|
|
|
|
2023-08-08 00:50:01 +00:00
|
|
|
console.info('nostr extension detected')
|
2023-11-12 17:59:18 +00:00
|
|
|
|
2023-08-08 00:50:01 +00:00
|
|
|
let mounted = true;
|
|
|
|
(async function () {
|
|
|
|
try {
|
|
|
|
// have them sign a message with the challenge
|
|
|
|
let event
|
|
|
|
try {
|
2023-12-19 22:01:48 +00:00
|
|
|
event = await callWithTimeout(() => window.nostr.signEvent({
|
2023-08-08 00:50:01 +00:00
|
|
|
kind: 22242,
|
|
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
|
|
tags: [['challenge', k1]],
|
|
|
|
content: 'Stacker News Authentication'
|
2023-12-19 22:01:48 +00:00
|
|
|
}), 5000)
|
2023-08-08 00:50:01 +00:00
|
|
|
if (!event) throw new Error('extension returned empty event')
|
|
|
|
} catch (e) {
|
2023-11-12 17:59:18 +00:00
|
|
|
if (e.message === 'window.nostr call already executing' || !mounted) return
|
2023-08-08 00:50:01 +00:00
|
|
|
setExtensionError({ message: 'nostr extension failed to sign event', details: e.message })
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// sign them in
|
|
|
|
try {
|
2023-11-12 17:59:18 +00:00
|
|
|
await signIn('nostr', {
|
2023-08-08 00:50:01 +00:00
|
|
|
event: JSON.stringify(event),
|
|
|
|
callbackUrl
|
|
|
|
})
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error('authorization failed', e)
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
if (!mounted) return
|
2023-11-12 17:59:18 +00:00
|
|
|
console.log('nostr auth error', e)
|
2023-08-08 00:50:01 +00:00
|
|
|
setExtensionError({ message: `${text} failed`, details: e.message })
|
|
|
|
}
|
|
|
|
})()
|
|
|
|
return () => { mounted = false }
|
|
|
|
}, [k1, hasExtension])
|
|
|
|
|
|
|
|
if (error) return <div>error</div>
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{hasExtension === false && <NostrExplainer text={text} />}
|
|
|
|
{extensionError && <ExtensionError {...extensionError} />}
|
|
|
|
{hasExtension && !extensionError &&
|
|
|
|
<>
|
|
|
|
<h4 className='fw-bold text-success pb-1'>nostr extension found</h4>
|
|
|
|
<h6 className='text-muted pb-4'>authorize event signature in extension</h6>
|
|
|
|
</>}
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default function NostrAuthWithExplainer ({ text, callbackUrl }) {
|
|
|
|
const router = useRouter()
|
|
|
|
return (
|
|
|
|
<Container>
|
|
|
|
<div className={styles.login}>
|
|
|
|
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
|
|
|
|
<h3 className='w-100 pb-2'>{text || 'Login'} with Nostr</h3>
|
|
|
|
<NostrAuth text={text} callbackUrl={callbackUrl} />
|
|
|
|
</div>
|
|
|
|
</Container>
|
|
|
|
)
|
|
|
|
}
|