diff --git a/CONTEXT.md b/CONTEXT.md
index d07ea2b..b9d6b82 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -152,28 +152,55 @@ The `useCurrentUser` hook should be used to ensure that the user is logged in be
### Nostr Login
-Nostr supports several types of logins:
-
-1. Login with nsec
-2. Login with browser extension
-3. Login with bunker URI
-
-Functions to log in with each of these methods are exposed by the `useLoginActions` hook:
+To add Nostr login functionality, use the included `LoginForm` and `SignupForm` dialog components. For example:
```tsx
-function MyComponent() {
- const login = useLoginActions();
+import LoginForm from "@/components/auth/LoginForm";
+import SignupForm from "@/components/auth/SignupForm";
+import { Button } from "@/components/ui/button";
- login.nsec(nsec); // login by the user pasting their secret key
- login.bunker(uri); // login by the user pasting a bunker URI
- login.extension(); // login with a NIP-07 browser extension
+function MyComponent() {
+ const [loginDialogOpen, setLoginDialogOpen] = useState(false);
+ const [signupDialogOpen, setSignupDialogOpen] = useState(false);
+
+ const handleLogin = () => {
+ setLoginDialogOpen(false);
+ setSignupDialogOpen(false);
+ };
return (
-
{/* ... */}
+
+ Log in
+ Sign up
+
+ setLoginDialogOpen(false)}
+ onLogin={handleLogin}
+ onSignup={showSignupDialog}
+ />
+
+ setSignupDialogOpen(false)}
+ />
+
);
}
```
+To access the currently-logged-in account, use the `useCurrentUser` hook, eg:
+
+```typescript
+import { useCurrentUser } from "@/hooks/useCurrentUser";
+
+function MyComponent() {
+ const { user } = useCurrentUser();
+
+ // ...
+}
+```
+
## Development Practices
- Uses React Query for data fetching and caching
diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx
new file mode 100644
index 0000000..9d86e9b
--- /dev/null
+++ b/src/components/auth/LoginForm.tsx
@@ -0,0 +1,223 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Shield, Upload } from 'lucide-react';
+import { Button } from '@/components/ui/button.tsx';
+import { Input } from '@/components/ui/input.tsx';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog.tsx';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs.tsx';
+import { useLoginActions } from '@/hooks/useLoginActions';
+
+interface LoginFormProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onLogin: () => void;
+ onSignup?: () => void;
+}
+
+const LoginForm: React.FC = ({ isOpen, onClose, onLogin, onSignup }) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [nsec, setNsec] = useState('');
+ const [bunkerUri, setBunkerUri] = useState('');
+ const [defaultTab, setDefaultTab] = useState('extension');
+ const fileInputRef = useRef(null);
+ const login = useLoginActions();
+
+ // Check if Nostr extension exists on component mount
+ useEffect(() => {
+ const hasNostrExtension = 'nostr' in window;
+ setDefaultTab(hasNostrExtension ? 'extension' : 'key');
+ }, []);
+
+ const handleExtensionLogin = () => {
+ setIsLoading(true);
+ try {
+ if (!('nostr' in window)) {
+ throw new Error('Nostr extension not found. Please install a NIP-07 extension.');
+ }
+ login.extension();
+ onLogin();
+ onClose();
+ } catch (error) {
+ console.error('Extension login failed:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleKeyLogin = () => {
+ if (!nsec.trim()) return;
+ setIsLoading(true);
+
+ try {
+ login.nsec(nsec);
+ onLogin();
+ onClose();
+ } catch (error) {
+ console.error('Nsec login failed:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleBunkerLogin = () => {
+ if (!bunkerUri.trim() || !bunkerUri.startsWith('bunker://')) return;
+ setIsLoading(true);
+
+ try {
+ login.bunker(bunkerUri);
+ onLogin();
+ onClose();
+ } catch (error) {
+ console.error('Bunker login failed:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleFileUpload = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const content = event.target?.result as string;
+ setNsec(content.trim());
+ };
+ reader.readAsText(file);
+ };
+
+ const handleSignupClick = () => {
+ onClose();
+ if (onSignup) {
+ onSignup();
+ }
+ };
+
+ return (
+
+
+
+ Log in
+
+ Access your account securely with your preferred method
+
+
+
+
+
+
+ Extension
+ Nsec
+ Bunker
+
+
+
+
+
+
+ Login with one click using the browser extension
+
+
+ {isLoading ? 'Logging in...' : 'Login with Extension'}
+
+
+
+
+
+
+
+
+ Enter your nsec
+
+ setNsec(e.target.value)}
+ className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary'
+ placeholder='nsec1...'
+ />
+
+
+
+
Or upload a key file
+
+
fileInputRef.current?.click()}
+ >
+
+ Upload Nsec File
+
+
+
+
+ {isLoading ? 'Verifying...' : 'Login with Nsec'}
+
+
+
+
+
+
+
+ Bunker URI
+
+
setBunkerUri(e.target.value)}
+ className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary'
+ placeholder='bunker://'
+ />
+ {bunkerUri && !bunkerUri.startsWith('bunker://') && (
+
URI must start with bunker://
+ )}
+
+
+
+ {isLoading ? 'Connecting...' : 'Login with Bunker'}
+
+
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+
+
+
+ );
+};
+
+export default LoginForm;
diff --git a/src/components/auth/SignupForm.tsx b/src/components/auth/SignupForm.tsx
new file mode 100644
index 0000000..e229d7f
--- /dev/null
+++ b/src/components/auth/SignupForm.tsx
@@ -0,0 +1,161 @@
+import React, { useState } from 'react';
+import { Download, Key } from 'lucide-react';
+import { Button } from '@/components/ui/button.tsx';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog.tsx';
+import { toast } from '@/hooks/useToast.ts';
+import { generateSecretKey, nip19 } from 'nostr-tools';
+
+interface SignupFormProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+const SignupForm: React.FC = ({ isOpen, onClose }) => {
+ const [step, setStep] = useState<'generate' | 'download' | 'done'>('generate');
+ const [isLoading, setIsLoading] = useState(false);
+ const [nsecKey, setNsecKey] = useState('');
+
+ // Generate a proper nsec key using nostr-tools
+ const generateKey = () => {
+ setIsLoading(true);
+
+ try {
+ // Generate a new private key
+ const privateKey = generateSecretKey();
+
+ // Convert to nsec format
+ const nsec = nip19.nsecEncode(privateKey);
+ setNsecKey(nsec);
+ setStep('download');
+ } catch (error) {
+ console.error('Failed to generate key:', error);
+ toast({
+ title: 'Error',
+ description: 'Failed to generate key. Please try again.',
+ variant: 'destructive',
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const downloadKey = () => {
+ // Create a blob with the key text
+ const blob = new Blob([nsecKey], { type: 'text/plain' });
+ const url = globalThis.URL.createObjectURL(blob);
+
+ // Create a temporary link element and trigger download
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'nsec.txt';
+ document.body.appendChild(a);
+ a.click();
+
+ // Clean up
+ globalThis.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+
+ toast({
+ title: 'Key downloaded',
+ description: 'Your key has been downloaded. Keep it safe!',
+ });
+ };
+
+ const finishSignup = () => {
+ setStep('done');
+ onClose();
+
+ toast({
+ title: 'Account created',
+ description: 'You are now logged in.',
+ });
+ };
+
+ return (
+
+
+
+
+ {step === 'generate' && 'Create Your Account'}
+ {step === 'download' && 'Download Your Key'}
+ {step === 'done' && 'Setting Up Your Account'}
+
+
+ {step === 'generate' && 'Generate a secure key for your account'}
+ {step === 'download' && "Keep your key safe - you'll need it to log in"}
+ {step === 'done' && 'Finalizing your account setup'}
+
+
+
+
+ {step === 'generate' && (
+
+
+
+
+
+ We'll generate a secure key for your account. You'll need this key to log in later.
+
+
+ {isLoading ? 'Generating key...' : 'Generate my key'}
+
+
+ )}
+
+ {step === 'download' && (
+
+
+ {nsecKey}
+
+
+
+
Important:
+
+ This is your only way to access your account
+ Store it somewhere safe
+ Never share this key with anyone
+
+
+
+
+
+
+ Download Key
+
+
+
+ I've saved my key, continue
+
+
+
+ )}
+
+ {step === 'done' && (
+
+ )}
+
+
+
+ );
+};
+
+export default SignupForm;