|
|
|
|
@ -1,10 +1,12 @@
@@ -1,10 +1,12 @@
|
|
|
|
|
import { Button } from '@/components/ui/button' |
|
|
|
|
import { Input } from '@/components/ui/input' |
|
|
|
|
import { DEFAULT_NOSTRCONNECT_RELAY } from '@/constants' |
|
|
|
|
import { cn } from '@/lib/utils' |
|
|
|
|
import { useNostr } from '@/providers/NostrProvider' |
|
|
|
|
import { createNostrConnectURI, NostrConnectParams } from 'nostr-tools/nip46' |
|
|
|
|
import { Check, Copy, Loader } from 'lucide-react' |
|
|
|
|
import { Check, Copy, Loader, ScanQrCode } from 'lucide-react' |
|
|
|
|
import { generateSecretKey, getPublicKey } from 'nostr-tools' |
|
|
|
|
import { createNostrConnectURI, NostrConnectParams } from 'nostr-tools/nip46' |
|
|
|
|
import QrScanner from 'qr-scanner' |
|
|
|
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react' |
|
|
|
|
import { useTranslation } from 'react-i18next' |
|
|
|
|
import QrCode from '../QrCode' |
|
|
|
|
@ -25,17 +27,22 @@ export default function NostrConnectLogin({
@@ -25,17 +27,22 @@ export default function NostrConnectLogin({
|
|
|
|
|
const [nostrConnectionErrMsg, setNostrConnectionErrMsg] = useState<string | null>(null) |
|
|
|
|
const qrContainerRef = useRef<HTMLDivElement>(null) |
|
|
|
|
const [qrCodeSize, setQrCodeSize] = useState(100) |
|
|
|
|
const [isScanning, setIsScanning] = useState(false) |
|
|
|
|
const videoRef = useRef<HTMLVideoElement>(null) |
|
|
|
|
const qrScannerRef = useRef<QrScanner | null>(null) |
|
|
|
|
const qrScannerCheckTimerRef = useRef<NodeJS.Timeout | null>(null) |
|
|
|
|
|
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
|
|
setBunkerInput(e.target.value) |
|
|
|
|
if (errMsg) setErrMsg(null) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const handleLogin = () => { |
|
|
|
|
if (bunkerInput === '') return |
|
|
|
|
const handleLogin = (bunker: string = bunkerInput) => { |
|
|
|
|
const _bunker = bunker.trim() |
|
|
|
|
if (_bunker.trim() === '') return |
|
|
|
|
|
|
|
|
|
setPending(true) |
|
|
|
|
bunkerLogin(bunkerInput) |
|
|
|
|
bunkerLogin(_bunker) |
|
|
|
|
.then(() => onLoginSuccess()) |
|
|
|
|
.catch((err) => setErrMsg(err.message || 'Login failed')) |
|
|
|
|
.finally(() => setPending(false)) |
|
|
|
|
@ -103,8 +110,82 @@ export default function NostrConnectLogin({
@@ -103,8 +110,82 @@ export default function NostrConnectLogin({
|
|
|
|
|
setTimeout(() => setCopied(false), 2000) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const startQrScan = async () => { |
|
|
|
|
try { |
|
|
|
|
setIsScanning(true) |
|
|
|
|
setErrMsg(null) |
|
|
|
|
|
|
|
|
|
// Wait for next render cycle to ensure video element is in DOM
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100)) |
|
|
|
|
|
|
|
|
|
if (!videoRef.current) { |
|
|
|
|
throw new Error('Video element not found') |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const hasCamera = await QrScanner.hasCamera() |
|
|
|
|
if (!hasCamera) { |
|
|
|
|
throw new Error('No camera found') |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const qrScanner = new QrScanner( |
|
|
|
|
videoRef.current, |
|
|
|
|
(result) => { |
|
|
|
|
setBunkerInput(result.data) |
|
|
|
|
stopQrScan() |
|
|
|
|
handleLogin(result.data) |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
highlightScanRegion: true, |
|
|
|
|
highlightCodeOutline: true, |
|
|
|
|
preferredCamera: 'environment' |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
qrScannerRef.current = qrScanner |
|
|
|
|
await qrScanner.start() |
|
|
|
|
|
|
|
|
|
// Check video feed after a delay
|
|
|
|
|
qrScannerCheckTimerRef.current = setTimeout(() => { |
|
|
|
|
if ( |
|
|
|
|
videoRef.current && |
|
|
|
|
(videoRef.current.videoWidth === 0 || videoRef.current.videoHeight === 0) |
|
|
|
|
) { |
|
|
|
|
setErrMsg('Camera feed not available') |
|
|
|
|
} |
|
|
|
|
}, 1000) |
|
|
|
|
} catch (error) { |
|
|
|
|
setErrMsg( |
|
|
|
|
`Failed to start camera: ${error instanceof Error ? error.message : 'Unknown error'}. Please check permissions.` |
|
|
|
|
) |
|
|
|
|
setIsScanning(false) |
|
|
|
|
if (qrScannerCheckTimerRef.current) { |
|
|
|
|
clearTimeout(qrScannerCheckTimerRef.current) |
|
|
|
|
qrScannerCheckTimerRef.current = null |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const stopQrScan = () => { |
|
|
|
|
if (qrScannerRef.current) { |
|
|
|
|
qrScannerRef.current.stop() |
|
|
|
|
qrScannerRef.current.destroy() |
|
|
|
|
qrScannerRef.current = null |
|
|
|
|
} |
|
|
|
|
setIsScanning(false) |
|
|
|
|
if (qrScannerCheckTimerRef.current) { |
|
|
|
|
clearTimeout(qrScannerCheckTimerRef.current) |
|
|
|
|
qrScannerCheckTimerRef.current = null |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
return () => { |
|
|
|
|
stopQrScan() |
|
|
|
|
} |
|
|
|
|
}, []) |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<> |
|
|
|
|
<div className="relative flex flex-col gap-4"> |
|
|
|
|
<div ref={qrContainerRef} className="flex flex-col items-center w-full space-y-3 mb-3"> |
|
|
|
|
<a href={loginDetails.connectionString} aria-label="Open with Nostr signer app"> |
|
|
|
|
<QrCode size={qrCodeSize} value={loginDetails.connectionString} /> |
|
|
|
|
@ -138,22 +219,52 @@ export default function NostrConnectLogin({
@@ -138,22 +219,52 @@ export default function NostrConnectLogin({
|
|
|
|
|
|
|
|
|
|
<div className="w-full space-y-1"> |
|
|
|
|
<div className="flex items-start space-x-2"> |
|
|
|
|
<Input |
|
|
|
|
placeholder="bunker://..." |
|
|
|
|
value={bunkerInput} |
|
|
|
|
onChange={handleInputChange} |
|
|
|
|
className={errMsg ? 'border-destructive' : ''} |
|
|
|
|
/> |
|
|
|
|
<Button onClick={handleLogin} disabled={pending}> |
|
|
|
|
<div className="flex-1 relative"> |
|
|
|
|
<Input |
|
|
|
|
placeholder="bunker://..." |
|
|
|
|
value={bunkerInput} |
|
|
|
|
onChange={handleInputChange} |
|
|
|
|
className={errMsg ? 'border-destructive pr-10' : 'pr-10'} |
|
|
|
|
/> |
|
|
|
|
<Button |
|
|
|
|
size="sm" |
|
|
|
|
variant="ghost" |
|
|
|
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0" |
|
|
|
|
onClick={startQrScan} |
|
|
|
|
disabled={pending} |
|
|
|
|
> |
|
|
|
|
<ScanQrCode /> |
|
|
|
|
</Button> |
|
|
|
|
</div> |
|
|
|
|
<Button onClick={() => handleLogin()} disabled={pending}> |
|
|
|
|
<Loader className={pending ? 'animate-spin mr-2' : 'hidden'} /> |
|
|
|
|
{t('Login')} |
|
|
|
|
</Button> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
{errMsg && <div className="text-xs text-destructive pl-3 pt-1">{errMsg}</div>} |
|
|
|
|
</div> |
|
|
|
|
<Button variant="secondary" onClick={back} className="w-full"> |
|
|
|
|
{t('Back')} |
|
|
|
|
</Button> |
|
|
|
|
</> |
|
|
|
|
|
|
|
|
|
<div className={cn('w-full h-full flex justify-center', isScanning ? '' : 'hidden')}> |
|
|
|
|
<video |
|
|
|
|
ref={videoRef} |
|
|
|
|
className="absolute inset-0 w-full h-full bg-background" |
|
|
|
|
autoPlay |
|
|
|
|
playsInline |
|
|
|
|
muted |
|
|
|
|
/> |
|
|
|
|
<Button |
|
|
|
|
variant="secondary" |
|
|
|
|
size="sm" |
|
|
|
|
className="absolute top-2 right-2" |
|
|
|
|
onClick={stopQrScan} |
|
|
|
|
> |
|
|
|
|
Cancel |
|
|
|
|
</Button> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|