diff --git a/CONTEXT.md b/CONTEXT.md
index 092b826..8b75b33 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -495,25 +495,89 @@ function MyComponent() {
}
```
-The `LoginArea` component handles all the login-related UI and interactions, including displaying login dialogs and switching between accounts. It should not be wrapped in any conditional logic.
+The `LoginArea` component handles all the login-related UI and interactions, including displaying login dialogs, sign up functionality, and switching between accounts. It should not be wrapped in any conditional logic.
-`LoginArea` displays a "Log in" button when the user is logged out, and changes to an account switcher once the user is logged in. It is an inline-flex element by default. To make it expand to the width of its container, you can pass a className like `flex` (to make it a block element) or `w-full`. If it is left as inline-flex, it's recommended to set a max width.
+`LoginArea` displays both "Log in" and "Sign Up" buttons when the user is logged out, and changes to an account switcher once the user is logged in. It is an inline-flex element by default. To make it expand to the width of its container, you can pass a className like `flex` (to make it a block element) or `w-full`. If it is left as inline-flex, it's recommended to set a max width.
**Important**: Social applications should include a profile menu button in the main interface (typically in headers/navigation) to provide access to account settings, profile editing, and logout functionality. Don't only show `LoginArea` in logged-out states.
### `npub`, `naddr`, and other Nostr addresses
-Nostr defines a set identifiers in NIP-19. Their prefixes:
+Nostr defines a set of bech32-encoded identifiers in NIP-19. Their prefixes and purposes:
-- `npub`: public keys
-- `nsec`: private keys
-- `note`: note ids
-- `nprofile`: a nostr profile
-- `nevent`: a nostr event
-- `naddr`: a nostr replaceable event coordinate
-- `nrelay`: a nostr relay (deprecated)
+- `npub1`: **public keys** - Just the 32-byte public key, no additional metadata
+- `nsec1`: **private keys** - Secret keys (should never be displayed publicly)
+- `note1`: **event IDs** - Just the 32-byte event ID (hex), no additional metadata
+- `nevent1`: **event pointers** - Event ID plus optional relay hints and author pubkey
+- `nprofile1`: **profile pointers** - Public key plus optional relay hints and petname
+- `naddr1`: **addressable event coordinates** - For parameterized replaceable events (kind 30000-39999)
+- `nrelay1`: **relay references** - Relay URLs (deprecated)
-NIP-19 identifiers include a prefix, the number "1", then a base32-encoded data string.
+#### Key Differences Between Similar Identifiers
+
+**`note1` vs `nevent1`:**
+- `note1`: Contains only the event ID (32 bytes) - specifically for kind:1 events (Short Text Notes) as defined in NIP-10
+- `nevent1`: Contains event ID plus optional relay hints and author pubkey - for any event kind
+- Use `note1` for simple references to text notes and threads
+- Use `nevent1` when you need to include relay hints or author context for any event type
+
+**`npub1` vs `nprofile1`:**
+- `npub1`: Contains only the public key (32 bytes)
+- `nprofile1`: Contains public key plus optional relay hints and petname
+- Use `npub1` for simple user references
+- Use `nprofile1` when you need to include relay hints or display name context
+
+#### NIP-19 Routing Implementation
+
+**Critical**: NIP-19 identifiers should be handled at the **root level** of URLs (e.g., `/note1...`, `/npub1...`, `/naddr1...`), NOT nested under paths like `/note/note1...` or `/profile/npub1...`.
+
+This project includes a boilerplate `NIP19Page` component that provides the foundation for handling all NIP-19 identifier types at the root level. The component is configured in the routing system and ready for AI agents to populate with specific functionality.
+
+**How it works:**
+
+1. **Root-Level Route**: The route `/:nip19` in `AppRouter.tsx` catches all NIP-19 identifiers
+2. **Automatic Decoding**: The `NIP19Page` component automatically decodes the identifier using `nip19.decode()`
+3. **Type-Specific Sections**: Different sections are rendered based on the identifier type:
+ - `npub1`/`nprofile1`: Profile section with placeholder for profile view
+ - `note1`: Note section with placeholder for kind:1 text note view
+ - `nevent1`: Event section with placeholder for any event type view
+ - `naddr1`: Addressable event section with placeholder for articles, marketplace items, etc.
+4. **Error Handling**: Invalid, vacant, or unsupported identifiers show 404 NotFound page
+5. **Ready for Population**: Each section includes comments indicating where AI agents should implement specific functionality
+
+**Example URLs that work automatically:**
+- `/npub1abc123...` - User profile (needs implementation)
+- `/note1def456...` - Kind:1 text note (needs implementation)
+- `/nevent1ghi789...` - Any event with relay hints (needs implementation)
+- `/naddr1jkl012...` - Addressable event (needs implementation)
+
+**Features included:**
+- Basic NIP-19 identifier decoding and routing
+- Type-specific sections for different identifier types
+- Error handling for invalid identifiers
+- Responsive container structure
+- Comments indicating where to implement specific views
+
+**Error handling:**
+- Invalid NIP-19 format → 404 NotFound
+- Unsupported identifier types (like `nsec1`) → 404 NotFound
+- Empty or missing identifiers → 404 NotFound
+
+To implement NIP-19 routing in your Nostr application:
+
+1. **The NIP19Page boilerplate is already created** - populate sections with specific functionality
+2. **The route is already configured** in `AppRouter.tsx`
+3. **Error handling is built-in** - all edge cases show appropriate 404 responses
+4. **Add specific components** for profile views, event displays, etc. as needed
+
+#### Event Type Distinctions
+
+**`note1` identifiers** are specifically for **kind:1 events** (Short Text Notes) as defined in NIP-10: "Text Notes and Threads". These are the basic social media posts in Nostr.
+
+**`nevent1` identifiers** can reference any event kind and include additional metadata like relay hints and author pubkey. Use `nevent1` when:
+- The event is not a kind:1 text note
+- You need to include relay hints for better discoverability
+- You want to include author context
#### Use in Filters
@@ -555,22 +619,20 @@ const events = await nostr.query(
);
```
-#### Use in URL Paths
+#### Implementation Guidelines
-For URL routing, use NIP-19 identifiers as path parameters (e.g., `/:nip19`) to create secure, universal links to Nostr events. Decode the identifier and render the appropriate component based on the type:
-
-- Regular events: Use `/nevent1...` paths
-- Replaceable/addressable events: Use `/naddr1...` paths
-
-Always use `naddr` identifiers for addressable events instead of just the `d` tag value, as `naddr` contains the author pubkey needed to create secure filters. This prevents security issues where malicious actors could publish events with the same `d` tag to override content.
-
-```ts
-// Secure routing with naddr
-const decoded = nip19.decode(params.nip19);
-if (decoded.type === 'naddr' && decoded.data.kind === 30024) {
- // Render ArticlePage component
-}
-```
+1. **Always decode NIP-19 identifiers** before using them in queries
+2. **Use the appropriate identifier type** based on your needs:
+ - Use `note1` for kind:1 text notes specifically
+ - Use `nevent1` when including relay hints or for non-kind:1 events
+ - Use `naddr1` for addressable events (always includes author pubkey for security)
+3. **Handle different identifier types** appropriately:
+ - `npub1`/`nprofile1`: Display user profiles
+ - `note1`: Display kind:1 text notes specifically
+ - `nevent1`: Display any event with optional relay context
+ - `naddr1`: Display addressable events (articles, marketplace items, etc.)
+4. **Security considerations**: Always use `naddr1` for addressable events instead of just the `d` tag value, as `naddr1` contains the author pubkey needed to create secure filters
+5. **Error handling**: Gracefully handle invalid or unsupported NIP-19 identifiers with 404 responses
### Nostr Edit Profile
@@ -927,15 +989,30 @@ When users specify color schemes:
- Implement responsive design with Tailwind breakpoints
- Add hover and focus states for interactive elements
-## Writing Tests
+## Writing Tests vs Running Tests
-**⚠️ CRITICAL FOR AI ASSISTANTS**: **DO NOT WRITE TESTS** unless the user is experiencing a specific problem or explicitly requests tests. Writing unnecessary tests wastes significant time and money. Only create tests when:
+There is an important distinction between **writing new tests** and **running existing tests**:
-1. **The user is experiencing a bug** that requires testing to diagnose
-2. **The user explicitly asks for tests** to be written
-3. **Existing functionality is broken** and tests are needed to verify fixes
+### Writing Tests (Creating New Test Files)
+**Do not write tests** unless the user explicitly requests them in plain language. Writing unnecessary tests wastes significant time and money. Only create tests when:
-**Never proactively write tests** for new features or components. Focus on building functionality that works, not on testing it unless specifically requested.
+1. **The user explicitly asks for tests** to be written in their message
+2. **The user describes a specific bug in plain language** and requests tests to help diagnose it
+3. **The user says they are still experiencing a problem** that you have already attempted to solve (tests can help verify the fix)
+
+**Never write tests because:**
+- Tool results show test failures (these are not user requests)
+- You think tests would be helpful
+- New features or components are created
+- Existing functionality needs verification
+
+### Running Tests (Executing the Test Suite)
+**ALWAYS run the test script** after making any code changes. This is mandatory regardless of whether you wrote new tests or not.
+
+- **You must run the test script** using `js-dev__run_script` tool with the "test" parameter
+- **Your task is not complete** until the test script passes without errors
+- **This applies to all changes** - bug fixes, new features, refactoring, or any code modifications
+- **The test script includes** TypeScript compilation, ESLint checks, and existing test validation
### Test Setup
@@ -968,6 +1045,8 @@ describe('MyComponent', () => {
## Testing Your Changes
-Whenever you are finished modifying code, you must run the **test** script using the **js-dev__run_script** tool.
+**CRITICAL**: Whenever you are finished modifying code, you must run the **test** script using the **js-dev__run_script** tool.
-**Your task is not considered finished until this test passes without errors.**
\ No newline at end of file
+**Your task is not considered finished until this test passes without errors.**
+
+**This requirement applies regardless of whether you wrote new tests or not.** The test script validates the entire codebase, including TypeScript compilation, ESLint rules, and existing test suite.
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 3e16e45..60644a7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,8 +11,8 @@
"@fontsource-variable/inter": "^5.2.6",
"@getalby/sdk": "^5.1.1",
"@hookform/resolvers": "^3.9.0",
- "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.3",
- "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.7",
+ "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4",
+ "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
@@ -1249,9 +1249,9 @@
}
},
"node_modules/@jsr/nostrify__nostrify": {
- "version": "0.46.3",
- "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.46.3.tgz",
- "integrity": "sha512-zJpOrD8bbrJroLRJjESAJZX/ZKFCaGfoz5fSfLP+gIcTiPo8JpzlrFBF6mvaDI/Mdd+1WTBwlCcW9On8rUVH7w==",
+ "version": "0.46.4",
+ "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.46.4.tgz",
+ "integrity": "sha512-bYuYkw9+jnHIDEJD6i+w0p8xV1oBLr1RF5rS3qF2yTz1b8i50dXKMO2cLObjalAAO3RYbnib3l85t1ZgWLCzAA==",
"dependencies": {
"@jsr/nostrify__types": "^0.36.0",
"@jsr/scure__base": "^1.2.4",
@@ -1359,9 +1359,9 @@
},
"node_modules/@nostrify/nostrify": {
"name": "@jsr/nostrify__nostrify",
- "version": "0.46.3",
- "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.46.3.tgz",
- "integrity": "sha512-zJpOrD8bbrJroLRJjESAJZX/ZKFCaGfoz5fSfLP+gIcTiPo8JpzlrFBF6mvaDI/Mdd+1WTBwlCcW9On8rUVH7w==",
+ "version": "0.46.4",
+ "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.46.4.tgz",
+ "integrity": "sha512-bYuYkw9+jnHIDEJD6i+w0p8xV1oBLr1RF5rS3qF2yTz1b8i50dXKMO2cLObjalAAO3RYbnib3l85t1ZgWLCzAA==",
"dependencies": {
"@jsr/nostrify__types": "^0.36.0",
"@jsr/scure__base": "^1.2.4",
@@ -1374,11 +1374,11 @@
},
"node_modules/@nostrify/react": {
"name": "@jsr/nostrify__react",
- "version": "0.2.7",
- "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__react/0.2.7.tgz",
- "integrity": "sha512-36fAOOymf34KR2OfE4jXBojbZnPsrIzQDAXzE7dko8/Qj+0s8iKnZoZKg9DVIT2F6Lxhr6SUiTze8xBxuJbf1A==",
+ "version": "0.2.8",
+ "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__react/0.2.8.tgz",
+ "integrity": "sha512-9sCsMm1uwuGWU+NhgFf6LS/4JzeRXnGOz+ocA5X3/ncvxZhidPOEAU39QyygRLFo/JYG8OC5wlyH8w0Uejq+VQ==",
"dependencies": {
- "@jsr/nostrify__nostrify": "^0.46.3",
+ "@jsr/nostrify__nostrify": "^0.46.4",
"nostr-tools": "^2.13.0",
"react": "^18.0.0"
}
diff --git a/package.json b/package.json
index 05e46bd..8484860 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,8 @@
"@fontsource-variable/inter": "^5.2.6",
"@getalby/sdk": "^5.1.1",
"@hookform/resolvers": "^3.9.0",
- "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.3",
- "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.7",
+ "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4",
+ "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx
index b8875c6..2db53ee 100644
--- a/src/AppRouter.tsx
+++ b/src/AppRouter.tsx
@@ -2,6 +2,7 @@ import { BrowserRouter, Route, Routes } from "react-router-dom";
import { ScrollToTop } from "./components/ScrollToTop";
import Index from "./pages/Index";
+import { NIP19Page } from "./pages/NIP19Page";
import NotFound from "./pages/NotFound";
export function AppRouter() {
@@ -10,6 +11,8 @@ export function AppRouter() {
} />
+ {/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */}
+ } />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
} />
diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..ffa1f37
--- /dev/null
+++ b/src/components/ErrorBoundary.tsx
@@ -0,0 +1,113 @@
+import { Component, ErrorInfo, ReactNode } from 'react';
+
+interface ErrorBoundaryState {
+ hasError: boolean;
+ error: Error | null;
+ errorInfo: ErrorInfo | null;
+}
+
+interface ErrorBoundaryProps {
+ children: ReactNode;
+ fallback?: ReactNode;
+}
+
+
+
+export class ErrorBoundary extends Component {
+ constructor(props: ErrorBoundaryProps) {
+ super(props);
+ this.state = {
+ hasError: false,
+ error: null,
+ errorInfo: null,
+ };
+ }
+
+ static getDerivedStateFromError(error: Error): Partial {
+ return {
+ hasError: true,
+ error,
+ };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error('Error caught by ErrorBoundary:', error, errorInfo);
+
+ this.setState({
+ error,
+ errorInfo,
+ });
+ }
+
+ handleReset = () => {
+ this.setState({
+ hasError: false,
+ error: null,
+ errorInfo: null,
+ });
+ };
+
+ render() {
+ if (this.state.hasError) {
+ if (this.props.fallback) {
+ return this.props.fallback;
+ }
+
+ return (
+
+
+
+
+ Something went wrong
+
+
+ An unexpected error occurred. The error has been reported.
+
+
+
+
+
+
+ Error details
+
+
+
+ Message:
+
+ {this.state.error?.message}
+
+
+ {this.state.error?.stack && (
+
+ Stack trace:
+
+ {this.state.error.stack}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
\ No newline at end of file
diff --git a/src/components/auth/LoginArea.tsx b/src/components/auth/LoginArea.tsx
index 40d2f53..f84414a 100644
--- a/src/components/auth/LoginArea.tsx
+++ b/src/components/auth/LoginArea.tsx
@@ -2,7 +2,7 @@
// It is important that all functionality in this file is preserved, and should only be modified if explicitly requested.
import { useState } from 'react';
-import { User } from 'lucide-react';
+import { User, UserPlus } from 'lucide-react';
import { Button } from '@/components/ui/button.tsx';
import LoginDialog from './LoginDialog';
import SignupDialog from './SignupDialog';
@@ -29,18 +29,27 @@ export function LoginArea({ className }: LoginAreaProps) {
{currentUser ? (
setLoginDialogOpen(true)} />
) : (
-
+
+
+
)}
setLoginDialogOpen(false)}
+ isOpen={loginDialogOpen}
+ onClose={() => setLoginDialogOpen(false)}
onLogin={handleLogin}
onSignup={() => setSignupDialogOpen(true)}
/>
diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx
index 5e7e49c..ff40ec0 100644
--- a/src/components/auth/LoginDialog.tsx
+++ b/src/components/auth/LoginDialog.tsx
@@ -1,19 +1,15 @@
// NOTE: This file is stable and usually should not be modified.
// It is important that all functionality in this file is preserved, and should only be modified if explicitly requested.
-import React, { 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 React, { useRef, useState, useEffect } from 'react';
+import { Shield, Upload, AlertTriangle, UserPlus, KeyRound, Sparkles, Cloud } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Dialog, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Alert, AlertDescription } from '@/components/ui/alert';
import { useLoginActions } from '@/hooks/useLoginActions';
+import { cn } from '@/lib/utils';
interface LoginDialogProps {
isOpen: boolean;
@@ -22,54 +18,124 @@ interface LoginDialogProps {
onSignup?: () => void;
}
+const validateNsec = (nsec: string) => {
+ return /^nsec1[a-zA-Z0-9]{58}$/.test(nsec);
+};
+
+const validateBunkerUri = (uri: string) => {
+ return uri.startsWith('bunker://');
+};
+
const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onSignup }) => {
const [isLoading, setIsLoading] = useState(false);
+ const [isFileLoading, setIsFileLoading] = useState(false);
const [nsec, setNsec] = useState('');
const [bunkerUri, setBunkerUri] = useState('');
+ const [errors, setErrors] = useState<{
+ nsec?: string;
+ bunker?: string;
+ file?: string;
+ extension?: string;
+ }>({});
const fileInputRef = useRef(null);
const login = useLoginActions();
- const handleExtensionLogin = () => {
+ // Reset all state when dialog opens/closes
+ useEffect(() => {
+ if (isOpen) {
+ // Reset state when dialog opens
+ setIsLoading(false);
+ setIsFileLoading(false);
+ setNsec('');
+ setBunkerUri('');
+ setErrors({});
+ // Reset file input
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ }
+ }, [isOpen]);
+
+ const handleExtensionLogin = async () => {
setIsLoading(true);
+ setErrors(prev => ({ ...prev, extension: undefined }));
+
try {
if (!('nostr' in window)) {
throw new Error('Nostr extension not found. Please install a NIP-07 extension.');
}
- login.extension();
+ await login.extension();
onLogin();
onClose();
- } catch (error) {
+ } catch (e: unknown) {
+ const error = e as Error;
+ console.error('Bunker login failed:', error);
+ console.error('Nsec login failed:', error);
console.error('Extension login failed:', error);
+ setErrors(prev => ({
+ ...prev,
+ extension: error instanceof Error ? error.message : 'Extension login failed'
+ }));
} finally {
setIsLoading(false);
}
};
+ const executeLogin = (key: string) => {
+ setIsLoading(true);
+ setErrors({});
+
+ // Use a timeout to allow the UI to update before the synchronous login call
+ setTimeout(() => {
+ try {
+ login.nsec(key);
+ onLogin();
+ onClose();
+ } catch {
+ setErrors({ nsec: "Failed to login with this key. Please check that it's correct." });
+ setIsLoading(false);
+ }
+ }, 50);
+ };
+
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);
+ if (!nsec.trim()) {
+ setErrors(prev => ({ ...prev, nsec: 'Please enter your secret key' }));
+ return;
}
+
+ if (!validateNsec(nsec)) {
+ setErrors(prev => ({ ...prev, nsec: 'Invalid secret key format. Must be a valid nsec starting with nsec1.' }));
+ return;
+ }
+ executeLogin(nsec);
};
- const handleBunkerLogin = () => {
- if (!bunkerUri.trim() || !bunkerUri.startsWith('bunker://')) return;
+ const handleBunkerLogin = async () => {
+ if (!bunkerUri.trim()) {
+ setErrors(prev => ({ ...prev, bunker: 'Please enter a bunker URI' }));
+ return;
+ }
+
+ if (!validateBunkerUri(bunkerUri)) {
+ setErrors(prev => ({ ...prev, bunker: 'Invalid bunker URI format. Must start with bunker://' }));
+ return;
+ }
+
setIsLoading(true);
-
+ setErrors(prev => ({ ...prev, bunker: undefined }));
+
try {
- login.bunker(bunkerUri);
+ await login.bunker(bunkerUri);
onLogin();
onClose();
- } catch (error) {
- console.error('Bunker login failed:', error);
+ // Clear the URI from memory
+ setBunkerUri('');
+ } catch {
+ setErrors(prev => ({
+ ...prev,
+ bunker: 'Failed to connect to bunker. Please check the URI.'
+ }));
} finally {
setIsLoading(false);
}
@@ -79,10 +145,27 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onS
const file = e.target.files?.[0];
if (!file) return;
+ setIsFileLoading(true);
+ setErrors({});
+
const reader = new FileReader();
reader.onload = (event) => {
+ setIsFileLoading(false);
const content = event.target?.result as string;
- setNsec(content.trim());
+ if (content) {
+ const trimmedContent = content.trim();
+ if (validateNsec(trimmedContent)) {
+ executeLogin(trimmedContent);
+ } else {
+ setErrors({ file: 'File does not contain a valid secret key.' });
+ }
+ } else {
+ setErrors({ file: 'Could not read file content.' });
+ }
+ };
+ reader.onerror = () => {
+ setIsFileLoading(false);
+ setErrors({ file: 'Failed to read file.' });
};
reader.readAsText(file);
};
@@ -94,58 +177,139 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onS
}
};
+ const defaultTab = 'nostr' in window ? 'extension' : 'key';
+
return (