mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00
nostr testing component for publishing workout events
This commit is contained in:
parent
173e4e31e4
commit
8e85ee4704
@ -1,102 +1,42 @@
|
||||
// app/(tabs)/library/programs.tsx
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { View, ScrollView, TextInput, ActivityIndicator, Platform, TouchableOpacity } from 'react-native';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, ScrollView, TextInput, ActivityIndicator, Platform, TouchableOpacity, Modal } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2,
|
||||
Code, Search, ListFilter, Wifi, Zap, FileJson
|
||||
Code, Search, ListFilter, Wifi, Zap, FileJson, X, Info
|
||||
} from 'lucide-react-native';
|
||||
import { useSQLiteContext } from 'expo-sqlite';
|
||||
import { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
|
||||
import { SQLTransaction, SQLResultSet, SQLError } from '@/lib/db/types';
|
||||
import { useNDK, useNDKAuth, useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
||||
import { schema } from '@/lib/db/schema';
|
||||
import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { getPublicKey } from 'nostr-tools';
|
||||
import { NostrEventKind } from '@/types/nostr';
|
||||
import { useNDKStore } from '@/lib/stores/ndk';
|
||||
|
||||
// Constants for Nostr
|
||||
const EVENT_KIND_EXERCISE = 33401;
|
||||
const EVENT_KIND_WORKOUT_TEMPLATE = 33402;
|
||||
const EVENT_KIND_WORKOUT_RECORD = 1301;
|
||||
// Define relay status
|
||||
enum RelayStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTING = 'connecting',
|
||||
CONNECTED = 'connected',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
// Simplified mock implementations for testing
|
||||
const generatePrivateKey = (): string => {
|
||||
// Generate a random hex string (32 bytes)
|
||||
return Array.from({ length: 64 }, () =>
|
||||
Math.floor(Math.random() * 16).toString(16)
|
||||
).join('');
|
||||
};
|
||||
|
||||
const getEventHash = (event: any): string => {
|
||||
// For testing, just create a mock hash
|
||||
const eventData = JSON.stringify([
|
||||
0,
|
||||
event.pubkey,
|
||||
event.created_at,
|
||||
event.kind,
|
||||
event.tags,
|
||||
event.content
|
||||
]);
|
||||
|
||||
// Simple hash function for demonstration
|
||||
return Array.from(eventData)
|
||||
.reduce((hash, char) => {
|
||||
return ((hash << 5) - hash) + char.charCodeAt(0);
|
||||
}, 0)
|
||||
.toString(16)
|
||||
.padStart(64, '0');
|
||||
};
|
||||
|
||||
const signEvent = (event: any, privateKey: string): string => {
|
||||
// In real implementation, this would sign the event hash with the private key
|
||||
// For testing, we'll just return a mock signature
|
||||
return Array.from({ length: 128 }, () =>
|
||||
Math.floor(Math.random() * 16).toString(16)
|
||||
).join('');
|
||||
};
|
||||
|
||||
interface NostrEvent {
|
||||
id?: string;
|
||||
// Interface for event display
|
||||
interface DisplayEvent {
|
||||
id: string;
|
||||
pubkey: string;
|
||||
created_at: number;
|
||||
kind: number;
|
||||
tags: string[][];
|
||||
created_at: number;
|
||||
content: string;
|
||||
tags: string[][];
|
||||
sig?: string;
|
||||
}
|
||||
|
||||
interface TableInfo {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface TableSchema {
|
||||
name: string;
|
||||
sql: string;
|
||||
}
|
||||
|
||||
interface SchemaVersion {
|
||||
version: number;
|
||||
}
|
||||
|
||||
interface ExerciseRow {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
category: string;
|
||||
equipment: string | null;
|
||||
description: string | null;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
format_json: string;
|
||||
format_units_json: string;
|
||||
}
|
||||
|
||||
// Default available filters for programs - can be adjusted later
|
||||
// Default available filters for programs
|
||||
const availableFilters = {
|
||||
equipment: ['Barbell', 'Dumbbell', 'Bodyweight', 'Machine', 'Cables', 'Other'],
|
||||
tags: ['Strength', 'Cardio', 'Mobility', 'Recovery'],
|
||||
@ -121,7 +61,7 @@ export default function ProgramsScreen() {
|
||||
initialized: false,
|
||||
tables: [],
|
||||
});
|
||||
const [schemas, setSchemas] = useState<TableSchema[]>([]);
|
||||
const [schemas, setSchemas] = useState<{name: string, sql: string}[]>([]);
|
||||
const [testResults, setTestResults] = useState<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
@ -132,35 +72,48 @@ export default function ProgramsScreen() {
|
||||
const [activeFilters, setActiveFilters] = useState(0);
|
||||
|
||||
// Nostr state
|
||||
const [relayUrl, setRelayUrl] = useState('ws://localhost:7777');
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [relayStatus, setRelayStatus] = useState<RelayStatus>(RelayStatus.DISCONNECTED);
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
const [events, setEvents] = useState<NostrEvent[]>([]);
|
||||
const [events, setEvents] = useState<DisplayEvent[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [privateKey, setPrivateKey] = useState('');
|
||||
const [publicKey, setPublicKey] = useState('');
|
||||
const [useGeneratedKeys, setUseGeneratedKeys] = useState(true);
|
||||
const [eventKind, setEventKind] = useState(EVENT_KIND_EXERCISE);
|
||||
const [eventKind, setEventKind] = useState(NostrEventKind.EXERCISE);
|
||||
const [eventContent, setEventContent] = useState('');
|
||||
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
|
||||
const [privateKey, setPrivateKey] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// WebSocket reference
|
||||
const socketRef = useRef<WebSocket | null>(null);
|
||||
// Use the NDK hooks
|
||||
const { ndk, isLoading: ndkLoading } = useNDK();
|
||||
const { currentUser, isAuthenticated } = useNDKCurrentUser();
|
||||
const { login, logout, generateKeys } = useNDKAuth();
|
||||
|
||||
// Tab state
|
||||
const [activeTab, setActiveTab] = useState('database');
|
||||
|
||||
useEffect(() => {
|
||||
// Check database status
|
||||
checkDatabase();
|
||||
inspectDatabase();
|
||||
generateKeys();
|
||||
}, []);
|
||||
|
||||
// Update relay status when NDK changes
|
||||
if (ndk) {
|
||||
setRelayStatus(RelayStatus.CONNECTED);
|
||||
setStatusMessage(isAuthenticated
|
||||
? `Connected as ${currentUser?.npub?.slice(0, 8)}...`
|
||||
: 'Connected to relays via NDK');
|
||||
} else if (ndkLoading) {
|
||||
setRelayStatus(RelayStatus.CONNECTING);
|
||||
setStatusMessage('Connecting to relays...');
|
||||
} else {
|
||||
setRelayStatus(RelayStatus.DISCONNECTED);
|
||||
setStatusMessage('Not connected');
|
||||
}
|
||||
}, [ndk, ndkLoading, isAuthenticated, currentUser]);
|
||||
|
||||
// DATABASE FUNCTIONS
|
||||
|
||||
const inspectDatabase = async () => {
|
||||
try {
|
||||
const result = await db.getAllAsync<TableSchema>(
|
||||
const result = await db.getAllAsync<{name: string, sql: string}>(
|
||||
"SELECT name, sql FROM sqlite_master WHERE type='table'"
|
||||
);
|
||||
setSchemas(result);
|
||||
@ -172,12 +125,12 @@ export default function ProgramsScreen() {
|
||||
const checkDatabase = async () => {
|
||||
try {
|
||||
// Check schema_version table
|
||||
const version = await db.getFirstAsync<SchemaVersion>(
|
||||
const version = await db.getFirstAsync<{version: number}>(
|
||||
'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1'
|
||||
);
|
||||
|
||||
// Get all tables
|
||||
const tables = await db.getAllAsync<TableInfo>(
|
||||
const tables = await db.getAllAsync<{name: string}>(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
);
|
||||
|
||||
@ -193,6 +146,7 @@ export default function ProgramsScreen() {
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const resetDatabase = async () => {
|
||||
try {
|
||||
await db.withTransactionAsync(async () => {
|
||||
@ -231,9 +185,9 @@ export default function ProgramsScreen() {
|
||||
// Test exercise
|
||||
const testExercise = {
|
||||
title: "Test Squat",
|
||||
type: "strength" as ExerciseType,
|
||||
category: "Legs" as ExerciseCategory,
|
||||
equipment: "barbell" as Equipment,
|
||||
type: "strength",
|
||||
category: "Legs",
|
||||
equipment: "barbell",
|
||||
description: "Test exercise",
|
||||
tags: ["test", "legs"],
|
||||
format: {
|
||||
@ -241,8 +195,8 @@ export default function ProgramsScreen() {
|
||||
reps: true
|
||||
},
|
||||
format_units: {
|
||||
weight: "kg" as const,
|
||||
reps: "count" as const
|
||||
weight: "kg",
|
||||
reps: "count"
|
||||
}
|
||||
};
|
||||
|
||||
@ -280,7 +234,7 @@ export default function ProgramsScreen() {
|
||||
});
|
||||
|
||||
// Verify insert
|
||||
const result = await db.getFirstAsync<ExerciseRow>(
|
||||
const result = await db.getFirstAsync(
|
||||
"SELECT * FROM exercises WHERE id = ?",
|
||||
['test-1']
|
||||
);
|
||||
@ -308,184 +262,216 @@ export default function ProgramsScreen() {
|
||||
setActiveFilters(totalFilters);
|
||||
// Implement filtering logic for programs when available
|
||||
};
|
||||
|
||||
// NOSTR FUNCTIONS
|
||||
|
||||
// Handle login dialog
|
||||
const handleShowLogin = () => {
|
||||
setIsLoginSheetOpen(true);
|
||||
};
|
||||
|
||||
// Generate new keypair
|
||||
const generateKeys = () => {
|
||||
// Close login sheet
|
||||
const handleCloseLogin = () => {
|
||||
setIsLoginSheetOpen(false);
|
||||
};
|
||||
|
||||
// Handle key generation
|
||||
const handleGenerateKeys = async () => {
|
||||
try {
|
||||
const privKey = generatePrivateKey();
|
||||
// For getPublicKey, we can use a mock function that returns a valid-looking pubkey
|
||||
const pubKey = privKey.slice(0, 64); // Just use part of the private key for demo
|
||||
|
||||
setPrivateKey(privKey);
|
||||
setPublicKey(pubKey);
|
||||
setStatusMessage('Keys generated successfully');
|
||||
} catch (error) {
|
||||
setStatusMessage(`Error generating keys: ${error}`);
|
||||
const { nsec } = generateKeys();
|
||||
setPrivateKey(nsec);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('Failed to generate keys');
|
||||
console.error('Key generation error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to relay
|
||||
const connectToRelay = () => {
|
||||
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
|
||||
socketRef.current.close();
|
||||
}
|
||||
|
||||
setConnecting(true);
|
||||
setStatusMessage('Connecting to relay...');
|
||||
|
||||
try {
|
||||
const socket = new WebSocket(relayUrl);
|
||||
|
||||
socket.onopen = () => {
|
||||
setConnected(true);
|
||||
setConnecting(false);
|
||||
setStatusMessage('Connected to relay!');
|
||||
socketRef.current = socket;
|
||||
|
||||
// Subscribe to exercise-related events
|
||||
const subscriptionId = 'test-sub-' + Math.random().toString(36).substring(2, 15);
|
||||
const subscription = JSON.stringify([
|
||||
'REQ',
|
||||
subscriptionId,
|
||||
{ kinds: [EVENT_KIND_EXERCISE, EVENT_KIND_WORKOUT_TEMPLATE, EVENT_KIND_WORKOUT_RECORD], limit: 10 }
|
||||
]);
|
||||
socket.send(subscription);
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data[0] === 'EVENT' && data[1] && data[2]) {
|
||||
const nostrEvent = data[2];
|
||||
setEvents(prev => [nostrEvent, ...prev].slice(0, 50)); // Keep most recent 50 events
|
||||
} else if (data[0] === 'NOTICE') {
|
||||
setStatusMessage(`Relay message: ${data[1]}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing message:', error, event.data);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
setConnected(false);
|
||||
setConnecting(false);
|
||||
setStatusMessage('Disconnected from relay');
|
||||
};
|
||||
|
||||
socket.onerror = (error) => {
|
||||
setConnected(false);
|
||||
setConnecting(false);
|
||||
setStatusMessage(`Connection error: ${error}`);
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
} catch (error) {
|
||||
setConnecting(false);
|
||||
setStatusMessage(`Failed to connect: ${error}`);
|
||||
console.error('Connection setup error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Disconnect from relay
|
||||
const disconnectFromRelay = () => {
|
||||
if (socketRef.current) {
|
||||
socketRef.current.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Create and publish a new event
|
||||
const publishEvent = () => {
|
||||
if (!socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) {
|
||||
setStatusMessage('Not connected to a relay');
|
||||
|
||||
// Handle login
|
||||
const handleLogin = async () => {
|
||||
if (!privateKey.trim()) {
|
||||
setError('Please enter your private key or generate a new one');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!privateKey || !publicKey) {
|
||||
setStatusMessage('Need private and public keys to publish');
|
||||
return;
|
||||
|
||||
setError(null);
|
||||
try {
|
||||
const success = await login(privateKey);
|
||||
|
||||
if (success) {
|
||||
setPrivateKey('');
|
||||
handleCloseLogin();
|
||||
} else {
|
||||
setError('Failed to login with the provided key');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Handle logout
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await logout();
|
||||
setStatusMessage('Logged out');
|
||||
setEvents([]);
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
setStatusMessage(`Logout error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Publish an event
|
||||
const handlePublishEvent = async () => {
|
||||
if (!isAuthenticated || !ndk || !currentUser) {
|
||||
setStatusMessage('You must login first');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
console.log('Creating event...');
|
||||
|
||||
// Create event with required pubkey (no longer optional)
|
||||
const event: NostrEvent = {
|
||||
pubkey: publicKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: eventKind,
|
||||
tags: [],
|
||||
content: eventContent,
|
||||
};
|
||||
// Prepare tags based on event kind
|
||||
const tags: string[][] = [];
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
// A basic implementation for each event kind
|
||||
if (eventKind === EVENT_KIND_EXERCISE) {
|
||||
event.tags.push(['d', `exercise-${Date.now()}`]);
|
||||
event.tags.push(['title', 'Test Exercise']);
|
||||
event.tags.push(['format', 'weight', 'reps']);
|
||||
event.tags.push(['format_units', 'kg', 'count']);
|
||||
event.tags.push(['equipment', 'barbell']);
|
||||
} else if (eventKind === EVENT_KIND_WORKOUT_TEMPLATE) {
|
||||
event.tags.push(['d', `template-${Date.now()}`]);
|
||||
event.tags.push(['title', 'Test Workout Template']);
|
||||
event.tags.push(['type', 'strength']);
|
||||
} else if (eventKind === EVENT_KIND_WORKOUT_RECORD) {
|
||||
event.tags.push(['d', `workout-${Date.now()}`]);
|
||||
event.tags.push(['title', 'Test Workout Record']);
|
||||
event.tags.push(['start', `${Math.floor(Date.now() / 1000) - 3600}`]);
|
||||
event.tags.push(['end', `${Math.floor(Date.now() / 1000)}`]);
|
||||
// Add appropriate tags based on event kind
|
||||
if (eventKind === NostrEventKind.TEXT) {
|
||||
// For regular text notes, we can add some simple tags
|
||||
tags.push(
|
||||
['t', 'powr'], // Adding a hashtag
|
||||
['t', 'test'], // Another hashtag
|
||||
['client', 'POWR App'] // Client info
|
||||
);
|
||||
|
||||
// If no content was provided, use a default message
|
||||
if (!eventContent || eventContent.trim() === '') {
|
||||
setEventContent('Hello from POWR App - Test Note');
|
||||
}
|
||||
} else if (eventKind === NostrEventKind.EXERCISE) {
|
||||
// Your existing exercise event code
|
||||
const uniqueId = `exercise-${timestamp}`;
|
||||
tags.push(
|
||||
['d', uniqueId],
|
||||
['title', eventContent || 'Test Exercise'],
|
||||
// Rest of your tags...
|
||||
);
|
||||
}
|
||||
if (eventKind === NostrEventKind.EXERCISE) {
|
||||
const uniqueId = `exercise-${timestamp}`;
|
||||
tags.push(
|
||||
['d', uniqueId],
|
||||
['title', eventContent || 'Test Exercise'],
|
||||
['type', 'strength'],
|
||||
['category', 'Legs'],
|
||||
['format', 'weight', 'reps'],
|
||||
['format_units', 'kg', 'count'],
|
||||
['equipment', 'barbell'],
|
||||
['t', 'test'],
|
||||
['t', 'powr']
|
||||
);
|
||||
} else if (eventKind === NostrEventKind.TEMPLATE) {
|
||||
const uniqueId = `template-${timestamp}`;
|
||||
tags.push(
|
||||
['d', uniqueId],
|
||||
['title', eventContent || 'Test Workout Template'],
|
||||
['type', 'strength'],
|
||||
['t', 'strength'],
|
||||
['t', 'legs'],
|
||||
['t', 'powr'],
|
||||
// Add exercise references - these would normally reference real exercise events
|
||||
['exercise', `33401:exercise-${timestamp-1}`, '3', '10', 'normal']
|
||||
);
|
||||
} else if (eventKind === NostrEventKind.WORKOUT) {
|
||||
const uniqueId = `workout-${timestamp}`;
|
||||
const startTime = timestamp - 3600; // 1 hour ago
|
||||
tags.push(
|
||||
['d', uniqueId],
|
||||
['title', eventContent || 'Test Workout Record'],
|
||||
['start', `${startTime}`],
|
||||
['end', `${timestamp}`],
|
||||
['completed', 'true'],
|
||||
['t', 'powr'],
|
||||
// Add exercise data - these would normally reference real exercise events
|
||||
['exercise', `33401:exercise-${timestamp-1}`, '100', '10', '8', 'normal']
|
||||
);
|
||||
}
|
||||
|
||||
// Hash and sign
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, privateKey);
|
||||
// Use the NDK store's publishEvent function
|
||||
const event = await useNDKStore.getState().publishEvent(eventKind, eventContent, tags);
|
||||
|
||||
// Publish to relay
|
||||
const message = JSON.stringify(['EVENT', event]);
|
||||
socketRef.current.send(message);
|
||||
|
||||
setStatusMessage('Event published successfully!');
|
||||
setEventContent('');
|
||||
setLoading(false);
|
||||
if (event) {
|
||||
// Add the published event to our display list
|
||||
const displayEvent: DisplayEvent = {
|
||||
id: event.id || '',
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind || eventKind, // Add fallback to eventKind if kind is undefined
|
||||
created_at: event.created_at || Math.floor(Date.now() / 1000), // Add fallback timestamp
|
||||
content: event.content,
|
||||
tags: event.tags.map(tag => tag.map(item => String(item))),
|
||||
sig: event.sig
|
||||
};
|
||||
|
||||
setEvents(prev => [displayEvent, ...prev]);
|
||||
|
||||
// Clear content field
|
||||
setEventContent('');
|
||||
setStatusMessage('Event published successfully!');
|
||||
} else {
|
||||
setStatusMessage('Failed to publish event');
|
||||
}
|
||||
} catch (error) {
|
||||
setStatusMessage(`Error publishing event: ${error}`);
|
||||
console.error('Error publishing event:', error);
|
||||
setStatusMessage(`Error publishing: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Query events from relay
|
||||
const queryEvents = () => {
|
||||
if (!socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) {
|
||||
setStatusMessage('Not connected to a relay');
|
||||
// Query events from NDK
|
||||
const queryEvents = async () => {
|
||||
if (!ndk) {
|
||||
setStatusMessage('NDK not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setEvents([]);
|
||||
|
||||
try {
|
||||
setEvents([]);
|
||||
setLoading(true);
|
||||
// Create a filter for the specific kind
|
||||
const filter = { kinds: [eventKind as number], limit: 20 };
|
||||
|
||||
// Create a new subscription for the selected event kind
|
||||
const subscriptionId = 'query-' + Math.random().toString(36).substring(2, 15);
|
||||
const subscription = JSON.stringify([
|
||||
'REQ',
|
||||
subscriptionId,
|
||||
{ kinds: [eventKind], limit: 20 }
|
||||
]);
|
||||
// Use the NDK store's fetchEventsByFilter function
|
||||
const fetchedEvents = await useNDKStore.getState().fetchEventsByFilter(filter);
|
||||
|
||||
socketRef.current.send(subscription);
|
||||
setStatusMessage(`Querying events of kind ${eventKind}...`);
|
||||
|
||||
// Close this subscription after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
|
||||
socketRef.current.send(JSON.stringify(['CLOSE', subscriptionId]));
|
||||
setLoading(false);
|
||||
setStatusMessage(`Completed query for kind ${eventKind}`);
|
||||
}
|
||||
}, 5000);
|
||||
const displayEvents: DisplayEvent[] = [];
|
||||
fetchedEvents.forEach(event => {
|
||||
// Ensure we handle potentially undefined values
|
||||
displayEvents.push({
|
||||
id: event.id || '',
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind || eventKind, // Use eventKind as fallback
|
||||
created_at: event.created_at || Math.floor(Date.now() / 1000), // Use current time as fallback
|
||||
content: event.content,
|
||||
// Convert tags to string[][]
|
||||
tags: event.tags.map(tag => tag.map(item => String(item))),
|
||||
sig: event.sig
|
||||
});
|
||||
});
|
||||
|
||||
setEvents(displayEvents);
|
||||
setStatusMessage(`Fetched ${displayEvents.length} events`);
|
||||
} catch (error) {
|
||||
console.error('Error querying events:', error);
|
||||
setStatusMessage(`Error querying: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setStatusMessage(`Error querying events: ${error}`);
|
||||
}
|
||||
};
|
||||
return (
|
||||
@ -548,7 +534,6 @@ export default function ProgramsScreen() {
|
||||
<Text className={activeTab === 'nostr' ? 'text-white' : 'text-foreground'}>Nostr</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'database' && (
|
||||
<ScrollView className="flex-1 p-4">
|
||||
@ -688,102 +673,136 @@ export default function ProgramsScreen() {
|
||||
<View className="py-4 space-y-4">
|
||||
<Text className="text-lg font-semibold text-center mb-4">Nostr Integration Test</Text>
|
||||
|
||||
{/* Connection controls */}
|
||||
{/* Connection status and controls */}
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex-row items-center gap-2">
|
||||
<Wifi size={20} className="text-foreground" />
|
||||
<Text className="text-lg font-semibold">Relay Connection</Text>
|
||||
<Text className="text-lg font-semibold">Nostr Connection</Text>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input
|
||||
value={relayUrl}
|
||||
onChangeText={setRelayUrl}
|
||||
placeholder="wss://relay.example.com"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
<View className="flex-row gap-4">
|
||||
<View className="flex-row gap-4 mb-4">
|
||||
<Button
|
||||
onPress={connectToRelay}
|
||||
disabled={connecting || connected}
|
||||
className="flex-1"
|
||||
onPress={handleShowLogin}
|
||||
disabled={isAuthenticated || loading}
|
||||
>
|
||||
{connecting ? (
|
||||
<><ActivityIndicator size="small" color="#fff" /><Text className="text-white ml-2">Connecting...</Text></>
|
||||
) : (
|
||||
<Text className="text-white">Connect</Text>
|
||||
)}
|
||||
<Text className="text-primary-foreground">Login with Nostr</Text>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={disconnectFromRelay}
|
||||
disabled={!connected}
|
||||
variant="destructive"
|
||||
className="flex-1"
|
||||
onPress={handleLogout}
|
||||
disabled={!isAuthenticated || loading}
|
||||
>
|
||||
<Text className="text-white">Disconnect</Text>
|
||||
<Text className="text-destructive-foreground">Logout</Text>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<Text className={`mt-2 ${connected ? 'text-green-500' : 'text-red-500'}`}>
|
||||
Status: {connected ? 'Connected' : 'Disconnected'}
|
||||
<Text className={`mt-2 ${
|
||||
relayStatus === RelayStatus.CONNECTED
|
||||
? 'text-green-500'
|
||||
: relayStatus === RelayStatus.ERROR
|
||||
? 'text-red-500'
|
||||
: 'text-yellow-500'
|
||||
}`}>
|
||||
Status: {relayStatus}
|
||||
</Text>
|
||||
|
||||
{statusMessage ? (
|
||||
<Text className="mt-2 text-gray-500">{statusMessage}</Text>
|
||||
<Text className="mt-2 text-muted-foreground">{statusMessage}</Text>
|
||||
) : null}
|
||||
|
||||
{isAuthenticated && currentUser && (
|
||||
<View className="mt-4 p-4 rounded-lg bg-muted">
|
||||
<Text className="font-medium">Logged in as:</Text>
|
||||
<Text className="text-sm text-muted-foreground mt-1" numberOfLines={1}>{currentUser.npub}</Text>
|
||||
{currentUser.profile?.displayName && (
|
||||
<Text className="text-sm mt-1">{currentUser.profile.displayName}</Text>
|
||||
)}
|
||||
|
||||
{/* Display active relay */}
|
||||
<Text className="font-medium mt-3">Active Relay:</Text>
|
||||
<Text className="text-sm text-muted-foreground">wss://powr.duckdns.org</Text>
|
||||
<Text className="text-xs text-muted-foreground mt-1">
|
||||
Note: To publish to additional relays, uncomment them in stores/ndk.ts
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Keys */}
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex-row items-center gap-2">
|
||||
<Code size={20} className="text-foreground" />
|
||||
<Text className="text-lg font-semibold">Nostr Keys</Text>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<View className="flex-row items-center mb-4">
|
||||
<Switch
|
||||
checked={useGeneratedKeys}
|
||||
onCheckedChange={setUseGeneratedKeys}
|
||||
id="use-generated-keys"
|
||||
/>
|
||||
<Label htmlFor="use-generated-keys" className="ml-2">Use generated keys</Label>
|
||||
<Button
|
||||
onPress={generateKeys}
|
||||
className="ml-auto"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Text>Generate New Keys</Text>
|
||||
</Button>
|
||||
{/* Login Modal */}
|
||||
<Modal
|
||||
visible={isLoginSheetOpen}
|
||||
transparent={true}
|
||||
animationType="slide"
|
||||
onRequestClose={handleCloseLogin}
|
||||
>
|
||||
<View className="flex-1 justify-center items-center bg-black/50">
|
||||
<View className="bg-background rounded-lg w-[90%] max-w-md p-4">
|
||||
<View className="flex-row justify-between items-center mb-4">
|
||||
<Text className="text-lg font-bold">Login with Nostr</Text>
|
||||
<TouchableOpacity onPress={handleCloseLogin}>
|
||||
<X size={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View className="space-y-4">
|
||||
<Text>Enter your Nostr private key (nsec)</Text>
|
||||
<Input
|
||||
placeholder="nsec1..."
|
||||
value={privateKey}
|
||||
onChangeText={setPrivateKey}
|
||||
secureTextEntry
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<View className="p-3 bg-destructive/10 rounded-md border border-destructive">
|
||||
<Text className="text-destructive">{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="flex-row space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onPress={handleGenerateKeys}
|
||||
disabled={loading}
|
||||
className="flex-1"
|
||||
>
|
||||
<Text>Generate New Keys</Text>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={handleLogin}
|
||||
disabled={loading}
|
||||
className="flex-1"
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<Text className="text-primary-foreground">Login</Text>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View className="bg-secondary/30 p-3 rounded-md mt-4">
|
||||
<View className="flex-row items-center mb-2">
|
||||
<Info size={16} className="mr-2 text-muted-foreground" />
|
||||
<Text className="font-semibold">What is a Nostr Key?</Text>
|
||||
</View>
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
Nostr is a decentralized protocol where your private key (nsec) is your identity and password.
|
||||
Your private key is securely stored on your device and is never sent to any servers.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text className="mb-1 font-medium">Public Key:</Text>
|
||||
<Input
|
||||
value={publicKey}
|
||||
onChangeText={setPublicKey}
|
||||
placeholder="Public key (hex)"
|
||||
editable={!useGeneratedKeys}
|
||||
className={`mb-4 ${useGeneratedKeys ? 'opacity-70' : ''}`}
|
||||
/>
|
||||
|
||||
<Text className="mb-1 font-medium">Private Key:</Text>
|
||||
<Input
|
||||
value={privateKey}
|
||||
onChangeText={setPrivateKey}
|
||||
placeholder="Private key (hex)"
|
||||
editable={!useGeneratedKeys}
|
||||
className={`mb-2 ${useGeneratedKeys ? 'opacity-70' : ''}`}
|
||||
/>
|
||||
<Text className="text-xs text-muted-foreground">Note: Never share your private key in a production app</Text>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</View>
|
||||
</Modal>
|
||||
{/* Create Event */}
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
@ -794,29 +813,37 @@ export default function ProgramsScreen() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Text className="mb-1 font-medium">Event Kind:</Text>
|
||||
<View className="flex-row gap-2 mb-4">
|
||||
<View className="flex-row gap-2 mb-4 flex-wrap">
|
||||
<Button
|
||||
variant={eventKind === EVENT_KIND_EXERCISE ? "default" : "outline"}
|
||||
onPress={() => setEventKind(EVENT_KIND_EXERCISE)}
|
||||
variant={eventKind === NostrEventKind.TEXT ? "default" : "outline"}
|
||||
onPress={() => setEventKind(NostrEventKind.TEXT)}
|
||||
size="sm"
|
||||
>
|
||||
<Text className={eventKind === EVENT_KIND_EXERCISE ? "text-white" : ""}>Exercise</Text>
|
||||
<Text className={eventKind === NostrEventKind.TEXT ? "text-white" : ""}>Text Note</Text>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={eventKind === EVENT_KIND_WORKOUT_TEMPLATE ? "default" : "outline"}
|
||||
onPress={() => setEventKind(EVENT_KIND_WORKOUT_TEMPLATE)}
|
||||
variant={eventKind === NostrEventKind.EXERCISE ? "default" : "outline"}
|
||||
onPress={() => setEventKind(NostrEventKind.EXERCISE)}
|
||||
size="sm"
|
||||
>
|
||||
<Text className={eventKind === EVENT_KIND_WORKOUT_TEMPLATE ? "text-white" : ""}>Template</Text>
|
||||
<Text className={eventKind === NostrEventKind.EXERCISE ? "text-white" : ""}>Exercise</Text>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={eventKind === EVENT_KIND_WORKOUT_RECORD ? "default" : "outline"}
|
||||
onPress={() => setEventKind(EVENT_KIND_WORKOUT_RECORD)}
|
||||
variant={eventKind === NostrEventKind.TEMPLATE ? "default" : "outline"}
|
||||
onPress={() => setEventKind(NostrEventKind.TEMPLATE)}
|
||||
size="sm"
|
||||
>
|
||||
<Text className={eventKind === EVENT_KIND_WORKOUT_RECORD ? "text-white" : ""}>Workout</Text>
|
||||
<Text className={eventKind === NostrEventKind.TEMPLATE ? "text-white" : ""}>Template</Text>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={eventKind === NostrEventKind.WORKOUT ? "default" : "outline"}
|
||||
onPress={() => setEventKind(NostrEventKind.WORKOUT)}
|
||||
size="sm"
|
||||
>
|
||||
<Text className={eventKind === NostrEventKind.WORKOUT ? "text-white" : ""}>Workout</Text>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@ -832,8 +859,8 @@ export default function ProgramsScreen() {
|
||||
|
||||
<View className="flex-row gap-4">
|
||||
<Button
|
||||
onPress={publishEvent}
|
||||
disabled={!connected || loading}
|
||||
onPress={handlePublishEvent}
|
||||
disabled={!isAuthenticated || loading}
|
||||
className="flex-1"
|
||||
>
|
||||
{loading ? (
|
||||
@ -845,7 +872,7 @@ export default function ProgramsScreen() {
|
||||
|
||||
<Button
|
||||
onPress={queryEvents}
|
||||
disabled={!connected || loading}
|
||||
disabled={!isAuthenticated || loading}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
@ -874,13 +901,14 @@ export default function ProgramsScreen() {
|
||||
<Text className="text-muted-foreground">No events yet</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView className="p-4" style={{ maxHeight: 200 }}>
|
||||
<ScrollView className="p-4" style={{ maxHeight: 300 }}>
|
||||
{events.map((event, index) => (
|
||||
<View key={event.id || index} className="mb-4">
|
||||
<Text className="font-bold">
|
||||
Kind: {event.kind} | Created: {new Date(event.created_at * 1000).toLocaleString()}
|
||||
</Text>
|
||||
<Text className="mb-1">ID: {event.id}</Text>
|
||||
<Text className="mb-1">Pubkey: {event.pubkey.slice(0, 8)}...</Text>
|
||||
|
||||
{/* Display tags */}
|
||||
{event.tags && event.tags.length > 0 && (
|
||||
@ -897,6 +925,13 @@ export default function ProgramsScreen() {
|
||||
<Text className="font-medium">Content:</Text>
|
||||
<Text className="ml-2 mb-2">{event.content}</Text>
|
||||
|
||||
{/* Display signature */}
|
||||
{event.sig && (
|
||||
<Text className="text-xs text-muted-foreground">
|
||||
Signature: {event.sig.slice(0, 16)}...
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{index < events.length - 1 && <Separator className="my-2" />}
|
||||
</View>
|
||||
))}
|
||||
@ -904,7 +939,6 @@ export default function ProgramsScreen() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Event JSON Viewer */}
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
@ -932,7 +966,7 @@ export default function ProgramsScreen() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* How To Use Guide */}
|
||||
{/* Testing Guide */}
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex-row items-center gap-2">
|
||||
@ -943,13 +977,13 @@ export default function ProgramsScreen() {
|
||||
<CardContent>
|
||||
<Text className="font-medium mb-2">How to test Nostr integration:</Text>
|
||||
<View className="space-y-2">
|
||||
<Text>1. Start local strfry relay using:</Text>
|
||||
<Text className="ml-4 font-mono bg-muted p-2 rounded">./strfry relay</Text>
|
||||
<Text>2. Connect to the relay (ws://localhost:7777)</Text>
|
||||
<Text>3. Generate or enter Nostr keys</Text>
|
||||
<Text>4. Create and publish test events</Text>
|
||||
<Text>5. Query for existing events</Text>
|
||||
<Text className="mt-2 text-muted-foreground">For details, see the Nostr Integration Testing Guide</Text>
|
||||
<Text>1. Click "Login with Nostr" to authenticate</Text>
|
||||
<Text>2. On the login sheet, click "Generate New Keys" to create a new Nostr identity</Text>
|
||||
<Text>3. Login with the generated keys</Text>
|
||||
<Text>4. Select an event kind (Exercise, Template, or Workout)</Text>
|
||||
<Text>5. Enter optional content and click "Publish"</Text>
|
||||
<Text>6. Use "Query Events" to fetch existing events of the selected kind</Text>
|
||||
<Text className="mt-2 text-muted-foreground">Using NDK for Nostr integration provides a more reliable experience than direct WebSocket connections.</Text>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
@ -1,4 +1,5 @@
|
||||
// app/_layout.tsx
|
||||
import '../lib/crypto-polyfill'; // Import crypto polyfill first
|
||||
import '@/global.css';
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
import { Stack } from 'expo-router';
|
||||
@ -17,6 +18,8 @@ import SettingsDrawer from '@/components/SettingsDrawer';
|
||||
import { useNDKStore } from '@/lib/stores/ndk';
|
||||
import { useWorkoutStore } from '@/stores/workoutStore';
|
||||
|
||||
console.log('_layout.tsx loaded');
|
||||
|
||||
const LIGHT_THEME = {
|
||||
...DefaultTheme,
|
||||
colors: NAV_THEME.light,
|
||||
|
@ -1,12 +1,10 @@
|
||||
// components/sheets/NostrLoginSheet.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { View, StyleSheet, Alert, Platform, KeyboardAvoidingView, ScrollView } from 'react-native';
|
||||
import { Info, ArrowRight } from 'lucide-react-native';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { Modal, View, StyleSheet, Platform, KeyboardAvoidingView, ScrollView, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||
import { Info, X } from 'lucide-react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useNDKAuth } from '@/lib/hooks/useNDK';
|
||||
|
||||
interface NostrLoginSheetProps {
|
||||
@ -16,14 +14,29 @@ interface NostrLoginSheetProps {
|
||||
|
||||
export default function NostrLoginSheet({ open, onClose }: NostrLoginSheetProps) {
|
||||
const [privateKey, setPrivateKey] = useState('');
|
||||
const { login, isLoading } = useNDKAuth();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { login, generateKeys, isLoading } = useNDKAuth();
|
||||
|
||||
// Handle key generation
|
||||
const handleGenerateKeys = async () => {
|
||||
try {
|
||||
const { nsec } = generateKeys();
|
||||
setPrivateKey(nsec);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('Failed to generate keys');
|
||||
console.error('Key generation error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle login
|
||||
const handleLogin = async () => {
|
||||
if (!privateKey.trim()) {
|
||||
Alert.alert('Error', 'Please enter your private key');
|
||||
setError('Please enter your private key or generate a new one');
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
try {
|
||||
const success = await login(privateKey);
|
||||
|
||||
@ -31,80 +44,137 @@ export default function NostrLoginSheet({ open, onClose }: NostrLoginSheetProps)
|
||||
setPrivateKey('');
|
||||
onClose();
|
||||
} else {
|
||||
Alert.alert('Login Error', 'Failed to login with the provided private key');
|
||||
setError('Failed to login with the provided key');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
Alert.alert('Error', 'An unexpected error occurred');
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
|
||||
}
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
isOpen={open}
|
||||
onClose={onClose}
|
||||
<Modal
|
||||
visible={open}
|
||||
transparent={true}
|
||||
animationType="slide"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<SheetContent>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.container}
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Login with Nostr</SheetTitle>
|
||||
{/* Removed the X close button here */}
|
||||
</SheetHeader>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Login with Nostr</Text>
|
||||
<TouchableOpacity onPress={onClose}>
|
||||
<X size={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.scrollView}>
|
||||
<View style={styles.content}>
|
||||
<Text className="mb-2">Enter your Nostr private key</Text>
|
||||
<Input
|
||||
placeholder="nsec1..."
|
||||
value={privateKey}
|
||||
onChangeText={setPrivateKey}
|
||||
secureTextEntry
|
||||
autoCapitalize="none"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
className="w-full mb-6"
|
||||
>
|
||||
<Text>{isLoading ? 'Logging in...' : 'Login'}</Text>
|
||||
{!isLoading}
|
||||
</Button>
|
||||
|
||||
<Separator className="mb-4" />
|
||||
|
||||
<View className="bg-secondary/30 p-3 rounded-md">
|
||||
<View className="flex-row items-center mb-2">
|
||||
<Info size={16} className="mr-3 text-muted-foreground" />
|
||||
<Text className="font-semibold">What is a Nostr Key?</Text>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.container}
|
||||
>
|
||||
<ScrollView style={styles.scrollView}>
|
||||
<View style={styles.content}>
|
||||
<Text className="mb-2 text-base">Enter your Nostr private key (nsec)</Text>
|
||||
<Input
|
||||
placeholder="nsec1..."
|
||||
value={privateKey}
|
||||
onChangeText={setPrivateKey}
|
||||
secureTextEntry
|
||||
autoCapitalize="none"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<View className="mb-4 p-3 bg-destructive/10 rounded-md border border-destructive">
|
||||
<Text className="text-destructive">{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="flex-row space-x-2 mb-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onPress={handleGenerateKeys}
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
<Text>Generate New Keys</Text>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color="#fff" style={styles.loader} />
|
||||
) : (
|
||||
<Text>Login</Text>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View className="bg-secondary/30 p-3 rounded-md">
|
||||
<View className="flex-row items-center mb-2">
|
||||
<Info size={16} className="mr-3 text-muted-foreground" />
|
||||
<Text className="font-semibold">What is a Nostr Key?</Text>
|
||||
</View>
|
||||
<Text className="text-sm text-muted-foreground mb-2">
|
||||
Nostr is a decentralized protocol where your private key (nsec) is your identity and password.
|
||||
</Text>
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
Your private key is securely stored on your device and is never sent to any servers.
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-sm text-muted-foreground mb-2">
|
||||
Nostr is a decentralized protocol where your private key (nsec) is your identity and password.
|
||||
</Text>
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
Your private key is securely stored on your device and is never sent to any servers.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
modalContent: {
|
||||
width: '90%',
|
||||
maxWidth: 500,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 15,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
container: {
|
||||
maxHeight: '80%',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
padding: 10,
|
||||
},
|
||||
loader: {
|
||||
marginRight: 8,
|
||||
}
|
||||
});
|
91
lib/crypto-polyfill.ts
Normal file
91
lib/crypto-polyfill.ts
Normal file
@ -0,0 +1,91 @@
|
||||
// lib/crypto-polyfill.ts
|
||||
import 'react-native-get-random-values';
|
||||
import * as Crypto from 'expo-crypto';
|
||||
|
||||
// Set up a more reliable polyfill
|
||||
export function setupCryptoPolyfill() {
|
||||
console.log('Setting up crypto polyfill...');
|
||||
|
||||
// Instead of using Object.defineProperty, let's use a different approach
|
||||
try {
|
||||
// First check if crypto exists and has getRandomValues
|
||||
if (typeof global.crypto === 'undefined') {
|
||||
(global as any).crypto = {};
|
||||
}
|
||||
|
||||
// Only define getRandomValues if it doesn't exist or isn't working
|
||||
if (!global.crypto.getRandomValues) {
|
||||
console.log('Defining getRandomValues implementation');
|
||||
(global.crypto as any).getRandomValues = function(array: Uint8Array) {
|
||||
console.log('Custom getRandomValues called');
|
||||
try {
|
||||
return Crypto.getRandomBytes(array.length);
|
||||
} catch (e) {
|
||||
console.error('Error in getRandomValues:', e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Test if it works
|
||||
const testArray = new Uint8Array(8);
|
||||
try {
|
||||
const result = global.crypto.getRandomValues(testArray);
|
||||
console.log('Crypto polyfill test result:', !!result);
|
||||
return true;
|
||||
} catch (testError) {
|
||||
console.error('Crypto test failed:', testError);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting up crypto polyfill:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Also expose a monkey-patching function for the specific libraries
|
||||
export function monkeyPatchNostrLibraries() {
|
||||
try {
|
||||
console.log('Attempting to monkey-patch nostr libraries...');
|
||||
|
||||
// Direct monkey patching of the randomBytes function in nostr libraries
|
||||
// This is an extreme approach but might be necessary
|
||||
const customRandomBytes = function(length: number): Uint8Array {
|
||||
console.log('Using custom randomBytes implementation');
|
||||
return Crypto.getRandomBytes(length);
|
||||
};
|
||||
|
||||
// Try to locate and patch the randomBytes function
|
||||
try {
|
||||
// Try to access the module using require
|
||||
const nobleHashes = require('@noble/hashes/utils');
|
||||
if (nobleHashes && nobleHashes.randomBytes) {
|
||||
console.log('Patching @noble/hashes/utils randomBytes');
|
||||
(nobleHashes as any).randomBytes = customRandomBytes;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not patch @noble/hashes/utils:', e);
|
||||
}
|
||||
|
||||
// Also try to patch nostr-tools if available
|
||||
try {
|
||||
const nostrTools = require('nostr-tools');
|
||||
if (nostrTools && nostrTools.crypto && nostrTools.crypto.randomBytes) {
|
||||
console.log('Patching nostr-tools crypto.randomBytes');
|
||||
(nostrTools.crypto as any).randomBytes = customRandomBytes;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not patch nostr-tools:', e);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error in monkey patching:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the polyfill
|
||||
setupCryptoPolyfill();
|
||||
// Try monkey patching as well
|
||||
monkeyPatchNostrLibraries();
|
@ -1,10 +1,19 @@
|
||||
// lib/hooks/useNDK.ts
|
||||
import { useEffect } from 'react';
|
||||
import { useNDKStore } from '../stores/ndk';
|
||||
import { useNDKStore } from '@/lib/stores/ndk';
|
||||
import type { NDKUser } from '@nostr-dev-kit/ndk';
|
||||
|
||||
/**
|
||||
* Hook to access NDK instance and initialization status
|
||||
*/
|
||||
export function useNDK() {
|
||||
const { ndk, isLoading, init } = useNDKStore();
|
||||
const { ndk, isLoading, error, init, relayStatus } = useNDKStore(state => ({
|
||||
ndk: state.ndk,
|
||||
isLoading: state.isLoading,
|
||||
error: state.error,
|
||||
init: state.init,
|
||||
relayStatus: state.relayStatus
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!ndk && !isLoading) {
|
||||
@ -12,15 +21,27 @@ export function useNDK() {
|
||||
}
|
||||
}, [ndk, isLoading, init]);
|
||||
|
||||
return { ndk, isLoading };
|
||||
return {
|
||||
ndk,
|
||||
isLoading,
|
||||
error,
|
||||
relayStatus
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access current NDK user information
|
||||
*/
|
||||
export function useNDKCurrentUser(): {
|
||||
currentUser: NDKUser | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
} {
|
||||
const { currentUser, isAuthenticated, isLoading } = useNDKStore();
|
||||
const { currentUser, isAuthenticated, isLoading } = useNDKStore(state => ({
|
||||
currentUser: state.currentUser,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
isLoading: state.isLoading
|
||||
}));
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
@ -29,13 +50,38 @@ export function useNDKCurrentUser(): {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access NDK authentication methods
|
||||
*/
|
||||
export function useNDKAuth() {
|
||||
const { login, logout, isAuthenticated, isLoading } = useNDKStore();
|
||||
const { login, logout, isAuthenticated, isLoading, generateKeys } = useNDKStore(state => ({
|
||||
login: state.login,
|
||||
logout: state.logout,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
isLoading: state.isLoading,
|
||||
generateKeys: state.generateKeys
|
||||
}));
|
||||
|
||||
return {
|
||||
login,
|
||||
logout,
|
||||
generateKeys,
|
||||
isAuthenticated,
|
||||
isLoading
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for direct access to Nostr event actions
|
||||
*/
|
||||
export function useNDKEvents() {
|
||||
const { publishEvent, fetchEventsByFilter } = useNDKStore(state => ({
|
||||
publishEvent: state.publishEvent,
|
||||
fetchEventsByFilter: state.fetchEventsByFilter
|
||||
}));
|
||||
|
||||
return {
|
||||
publishEvent,
|
||||
fetchEventsByFilter
|
||||
};
|
||||
}
|
@ -1,55 +1,85 @@
|
||||
// lib/hooks/useSubscribe.ts
|
||||
import { useEffect, useState } from 'react';
|
||||
import { NDKEvent, NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk-mobile';
|
||||
import { useNDK } from './useNDK';
|
||||
|
||||
interface UseSubscribeOptions {
|
||||
enabled?: boolean;
|
||||
closeOnEose?: boolean;
|
||||
deduplicate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to subscribe to Nostr events
|
||||
*
|
||||
* @param filters The NDK filter or array of filters
|
||||
* @param options Optional configuration options
|
||||
* @returns Object containing events, loading state, and EOSE status
|
||||
*/
|
||||
export function useSubscribe(
|
||||
filters: NDKFilter[] | false,
|
||||
filters: NDKFilter | NDKFilter[] | false,
|
||||
options: UseSubscribeOptions = {}
|
||||
) {
|
||||
const { ndk } = useNDK();
|
||||
const [events, setEvents] = useState<NDKEvent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [eose, setEose] = useState(false);
|
||||
const subscriptionRef = useRef<NDKSubscription | null>(null);
|
||||
|
||||
// Default options
|
||||
const { enabled = true, closeOnEose = false } = options;
|
||||
const {
|
||||
enabled = true,
|
||||
closeOnEose = false,
|
||||
deduplicate = true
|
||||
} = options;
|
||||
|
||||
useEffect(() => {
|
||||
// Clean up previous subscription if exists
|
||||
if (subscriptionRef.current) {
|
||||
subscriptionRef.current.stop();
|
||||
subscriptionRef.current = null;
|
||||
}
|
||||
|
||||
// Reset state when filters change
|
||||
setEvents([]);
|
||||
setEose(false);
|
||||
|
||||
// Check prerequisites
|
||||
if (!ndk || !filters || !enabled) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setEose(false);
|
||||
|
||||
let subscription: NDKSubscription;
|
||||
|
||||
try {
|
||||
subscription = ndk.subscribe(filters);
|
||||
// Convert single filter to array if needed
|
||||
const filterArray = Array.isArray(filters) ? filters : [filters];
|
||||
|
||||
// Create subscription
|
||||
const subscription = ndk.subscribe(filterArray);
|
||||
subscriptionRef.current = subscription;
|
||||
|
||||
// Handle incoming events
|
||||
subscription.on('event', (event: NDKEvent) => {
|
||||
setEvents(prev => {
|
||||
// Avoid duplicates
|
||||
if (prev.some(e => e.id === event.id)) {
|
||||
// Deduplicate events if enabled
|
||||
if (deduplicate && prev.some(e => e.id === event.id)) {
|
||||
return prev;
|
||||
}
|
||||
return [...prev, event];
|
||||
});
|
||||
});
|
||||
|
||||
// Handle end of stored events
|
||||
subscription.on('eose', () => {
|
||||
setIsLoading(false);
|
||||
setEose(true);
|
||||
|
||||
if (closeOnEose) {
|
||||
subscription.stop();
|
||||
if (closeOnEose && subscriptionRef.current) {
|
||||
subscriptionRef.current.stop();
|
||||
subscriptionRef.current = null;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@ -57,12 +87,27 @@ export function useSubscribe(
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (subscription) {
|
||||
subscription.stop();
|
||||
if (subscriptionRef.current) {
|
||||
subscriptionRef.current.stop();
|
||||
subscriptionRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [ndk, enabled, closeOnEose, JSON.stringify(filters)]);
|
||||
}, [ndk, enabled, closeOnEose, deduplicate, JSON.stringify(filters)]);
|
||||
|
||||
return { events, isLoading, eose };
|
||||
return {
|
||||
events,
|
||||
isLoading,
|
||||
eose,
|
||||
resubscribe: () => {
|
||||
if (subscriptionRef.current) {
|
||||
subscriptionRef.current.stop();
|
||||
subscriptionRef.current = null;
|
||||
}
|
||||
setEvents([]);
|
||||
setEose(false);
|
||||
setIsLoading(true);
|
||||
}
|
||||
};
|
||||
}
|
89
lib/mobile-signer.ts
Normal file
89
lib/mobile-signer.ts
Normal file
@ -0,0 +1,89 @@
|
||||
// lib/mobile-signer.ts
|
||||
import '../lib/crypto-polyfill'; // Import crypto polyfill first
|
||||
import * as Crypto from 'expo-crypto';
|
||||
import * as Random from 'expo-random';
|
||||
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
||||
import * as nostrTools from 'nostr-tools';
|
||||
import { setupCryptoPolyfill } from './crypto-polyfill';
|
||||
|
||||
/**
|
||||
* A custom signer implementation for React Native
|
||||
* Extends NDKPrivateKeySigner to handle different key formats
|
||||
*/
|
||||
export class NDKMobilePrivateKeySigner extends NDKPrivateKeySigner {
|
||||
constructor(privateKey: string) {
|
||||
// Handle different private key formats
|
||||
let hexKey = privateKey;
|
||||
|
||||
// Convert nsec to hex if needed
|
||||
if (privateKey.startsWith('nsec')) {
|
||||
try {
|
||||
const { type, data } = nostrTools.nip19.decode(privateKey);
|
||||
if (type === 'nsec') {
|
||||
// Handle the data as string (already in hex format)
|
||||
if (typeof data === 'string') {
|
||||
hexKey = data;
|
||||
}
|
||||
// Handle if it's a Uint8Array
|
||||
else if (data instanceof Uint8Array) {
|
||||
hexKey = bytesToHex(data);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Not an nsec key');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error processing nsec key:', e);
|
||||
throw new Error('Invalid private key format');
|
||||
}
|
||||
}
|
||||
|
||||
// Call the parent constructor with the hex key
|
||||
super(hexKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new Nostr keypair
|
||||
* Uses Expo's crypto functions directly instead of relying on polyfills
|
||||
*/
|
||||
// Add this to your generateKeyPair function
|
||||
export function generateKeyPair() {
|
||||
try {
|
||||
// Ensure crypto polyfill is set up
|
||||
if (typeof setupCryptoPolyfill === 'function') {
|
||||
setupCryptoPolyfill();
|
||||
}
|
||||
|
||||
let privateKeyBytes;
|
||||
|
||||
// Try expo-crypto first since expo-random is deprecated
|
||||
try {
|
||||
privateKeyBytes = Crypto.getRandomBytes(32);
|
||||
} catch (e) {
|
||||
console.warn('expo-crypto failed:', e);
|
||||
// Fallback to expo-random as last resort
|
||||
privateKeyBytes = Random.getRandomBytes(32);
|
||||
}
|
||||
|
||||
const privateKey = bytesToHex(privateKeyBytes);
|
||||
|
||||
// Get the public key from the private key using nostr-tools
|
||||
const publicKey = nostrTools.getPublicKey(privateKeyBytes);
|
||||
|
||||
// Encode keys in bech32 format
|
||||
const nsec = nostrTools.nip19.nsecEncode(privateKeyBytes);
|
||||
const npub = nostrTools.nip19.npubEncode(publicKey);
|
||||
|
||||
// Make sure we return the object with all properties
|
||||
return {
|
||||
privateKey,
|
||||
publicKey,
|
||||
nsec,
|
||||
npub
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[MobileSigner] Error generating key pair:', error);
|
||||
throw error; // Return the actual error for better debugging
|
||||
}
|
||||
}
|
@ -1,54 +1,116 @@
|
||||
// lib/stores/ndk.ts
|
||||
// stores/ndk.ts
|
||||
import '@/lib/crypto-polyfill'; // Import crypto polyfill first
|
||||
import { create } from 'zustand';
|
||||
import NDK, { NDKEvent, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
|
||||
import NDK, { NDKFilter, NDKEvent as NDKEventBase } from '@nostr-dev-kit/ndk';
|
||||
import { NDKUser } from '@nostr-dev-kit/ndk-mobile';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { NDKMobilePrivateKeySigner, generateKeyPair } from '@/lib/mobile-signer';
|
||||
import { setupCryptoPolyfill } from '@/lib/crypto-polyfill';
|
||||
|
||||
// Constants for SecureStore
|
||||
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
|
||||
|
||||
// Default relays
|
||||
const DEFAULT_RELAYS = [
|
||||
'ws://localhost:8080', // Add your local test relay
|
||||
//'wss://relay.damus.io',
|
||||
//'wss://relay.nostr.band',
|
||||
//'wss://purplepag.es',
|
||||
//'wss://nos.lol'
|
||||
'wss://powr.duckdns.org', // Your primary relay
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.nostr.band',
|
||||
'wss://nos.lol'
|
||||
];
|
||||
|
||||
// Helper function to convert Array/Uint8Array to hex string
|
||||
function arrayToHex(array: number[] | Uint8Array): string {
|
||||
return Array.from(array)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
type NDKStoreState = {
|
||||
ndk: NDK | null;
|
||||
currentUser: NDKUser | null;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
init: () => Promise<void>;
|
||||
login: (privateKey: string) => Promise<boolean>;
|
||||
logout: () => Promise<void>;
|
||||
getPublicKey: () => Promise<string | null>;
|
||||
error: Error | null;
|
||||
relayStatus: Record<string, 'connected' | 'connecting' | 'disconnected' | 'error'>;
|
||||
};
|
||||
|
||||
export const useNDKStore = create<NDKStoreState>((set, get) => ({
|
||||
type NDKStoreActions = {
|
||||
init: () => Promise<void>;
|
||||
login: (privateKey?: string) => Promise<boolean>;
|
||||
logout: () => Promise<void>;
|
||||
generateKeys: () => { privateKey: string; publicKey: string; nsec: string; npub: string };
|
||||
publishEvent: (kind: number, content: string, tags: string[][]) => Promise<NDKEvent | null>;
|
||||
fetchEventsByFilter: (filter: NDKFilter) => Promise<NDKEvent[]>;
|
||||
};
|
||||
|
||||
export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) => ({
|
||||
// State properties
|
||||
ndk: null,
|
||||
currentUser: null,
|
||||
isLoading: true,
|
||||
isLoading: false,
|
||||
isAuthenticated: false,
|
||||
|
||||
error: null,
|
||||
relayStatus: {},
|
||||
|
||||
// Initialize NDK
|
||||
init: async () => {
|
||||
try {
|
||||
console.log('[NDK] Initializing...');
|
||||
console.log('NDK init crypto polyfill check:', {
|
||||
cryptoDefined: typeof global.crypto !== 'undefined',
|
||||
getRandomValuesDefined: typeof global.crypto?.getRandomValues !== 'undefined'
|
||||
});
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
// Initialize relay status tracking
|
||||
const relayStatus: Record<string, 'connected' | 'connecting' | 'disconnected' | 'error'> = {};
|
||||
DEFAULT_RELAYS.forEach(r => {
|
||||
relayStatus[r] = 'connecting';
|
||||
});
|
||||
set({ relayStatus });
|
||||
|
||||
// Initialize NDK with relays
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: DEFAULT_RELAYS
|
||||
});
|
||||
|
||||
// Connect to relays
|
||||
await ndk.connect();
|
||||
|
||||
// Setup relay status updates
|
||||
DEFAULT_RELAYS.forEach(url => {
|
||||
const relay = ndk.pool.getRelay(url);
|
||||
if (relay) {
|
||||
relay.on('connect', () => {
|
||||
set(state => ({
|
||||
relayStatus: {
|
||||
...state.relayStatus,
|
||||
[url]: 'connected'
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
relay.on('disconnect', () => {
|
||||
set(state => ({
|
||||
relayStatus: {
|
||||
...state.relayStatus,
|
||||
[url]: 'disconnected'
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
// Set error status if not connected within timeout
|
||||
setTimeout(() => {
|
||||
set(state => {
|
||||
if (state.relayStatus[url] === 'connecting') {
|
||||
return {
|
||||
relayStatus: {
|
||||
...state.relayStatus,
|
||||
[url]: 'error'
|
||||
}
|
||||
};
|
||||
}
|
||||
return state;
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
|
||||
set({ ndk });
|
||||
|
||||
// Check for saved private key
|
||||
@ -57,8 +119,8 @@ export const useNDKStore = create<NDKStoreState>((set, get) => ({
|
||||
console.log('[NDK] Found saved private key, initializing signer');
|
||||
|
||||
try {
|
||||
// Create signer with private key
|
||||
const signer = new NDKPrivateKeySigner(privateKey);
|
||||
// Create mobile-specific signer with private key
|
||||
const signer = new NDKMobilePrivateKeySigner(privateKey);
|
||||
ndk.signer = signer;
|
||||
|
||||
// Get user and profile
|
||||
@ -78,61 +140,41 @@ export const useNDKStore = create<NDKStoreState>((set, get) => ({
|
||||
await SecureStore.deleteItemAsync(PRIVATE_KEY_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
set({ isLoading: false });
|
||||
} catch (error) {
|
||||
console.error('[NDK] Initialization error:', error);
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
set({
|
||||
error: error instanceof Error ? error : new Error('Failed to initialize NDK'),
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// lib/stores/ndk.ts - updated login method
|
||||
login: async (privateKey: string) => {
|
||||
set({ isLoading: true });
|
||||
login: async (privateKey?: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const { ndk } = get();
|
||||
if (!ndk) {
|
||||
console.error('[NDK] NDK not initialized');
|
||||
return false;
|
||||
throw new Error('NDK not initialized');
|
||||
}
|
||||
|
||||
// Process the private key (handle nsec format)
|
||||
let hexKey = privateKey;
|
||||
|
||||
if (privateKey.startsWith('nsec1')) {
|
||||
try {
|
||||
const { type, data } = nip19.decode(privateKey);
|
||||
if (type !== 'nsec') {
|
||||
throw new Error('Invalid nsec key');
|
||||
}
|
||||
|
||||
// Handle different data types
|
||||
if (typeof data === 'string') {
|
||||
hexKey = data;
|
||||
} else if (Array.isArray(data)) {
|
||||
// Convert array to hex string
|
||||
hexKey = arrayToHex(data);
|
||||
} else if (data instanceof Uint8Array) {
|
||||
// Convert Uint8Array to hex string
|
||||
hexKey = arrayToHex(data);
|
||||
} else {
|
||||
throw new Error('Unsupported key format');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[NDK] Key decode error:', error);
|
||||
throw new Error('Invalid private key format');
|
||||
}
|
||||
// If no private key is provided, generate one
|
||||
let userPrivateKey = privateKey;
|
||||
if (!userPrivateKey) {
|
||||
const { privateKey: generatedKey } = get().generateKeys();
|
||||
userPrivateKey = generatedKey;
|
||||
}
|
||||
|
||||
// Create signer with hex key
|
||||
console.log('[NDK] Creating signer with key');
|
||||
const signer = new NDKPrivateKeySigner(hexKey);
|
||||
// Create mobile-specific signer with private key
|
||||
const signer = new NDKMobilePrivateKeySigner(userPrivateKey);
|
||||
ndk.signer = signer;
|
||||
|
||||
// Get user
|
||||
const user = await ndk.signer.user();
|
||||
if (!user) {
|
||||
throw new Error('Failed to get user from signer');
|
||||
throw new Error('Could not get user from signer');
|
||||
}
|
||||
|
||||
// Fetch user profile
|
||||
@ -149,19 +191,22 @@ login: async (privateKey: string) => {
|
||||
}
|
||||
|
||||
// Save the private key securely
|
||||
await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, hexKey);
|
||||
await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, userPrivateKey);
|
||||
|
||||
set({
|
||||
currentUser: user,
|
||||
isAuthenticated: true
|
||||
isAuthenticated: true,
|
||||
isLoading: false
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[NDK] Login error:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error : new Error('Failed to login'),
|
||||
isLoading: false
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
@ -176,7 +221,7 @@ login: async (privateKey: string) => {
|
||||
ndk.signer = undefined;
|
||||
}
|
||||
|
||||
// Completely reset the user state
|
||||
// Reset the user state
|
||||
set({
|
||||
currentUser: null,
|
||||
isAuthenticated: false
|
||||
@ -188,11 +233,98 @@ login: async (privateKey: string) => {
|
||||
}
|
||||
},
|
||||
|
||||
getPublicKey: async () => {
|
||||
const { currentUser } = get();
|
||||
if (currentUser) {
|
||||
return currentUser.pubkey;
|
||||
generateKeys: () => {
|
||||
try {
|
||||
return generateKeyPair();
|
||||
} catch (error) {
|
||||
console.error('[NDK] Error generating keys:', error);
|
||||
set({ error: error instanceof Error ? error : new Error('Failed to generate keys') });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// In your publishEvent function in ndk.ts:
|
||||
publishEvent: async (kind: number, content: string, tags: string[][]) => {
|
||||
try {
|
||||
const { ndk, isAuthenticated, currentUser } = get();
|
||||
|
||||
if (!ndk) {
|
||||
throw new Error('NDK not initialized');
|
||||
}
|
||||
|
||||
if (!isAuthenticated || !currentUser) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
// Define custom functions we'll use to override crypto
|
||||
const customRandomBytes = (length: number): Uint8Array => {
|
||||
console.log('Using custom randomBytes in event signing');
|
||||
// Use type assertion to avoid TypeScript error
|
||||
return (Crypto as any).getRandomBytes(length);
|
||||
};
|
||||
|
||||
// Create event
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = kind;
|
||||
event.content = content;
|
||||
event.tags = tags;
|
||||
|
||||
// Direct monkey-patching approach
|
||||
try {
|
||||
// Try to find and override the randomBytes function
|
||||
const nostrTools = require('nostr-tools');
|
||||
const nobleHashes = require('@noble/hashes/utils');
|
||||
|
||||
// Backup original functions
|
||||
const originalNobleRandomBytes = nobleHashes.randomBytes;
|
||||
|
||||
// Override with our implementation
|
||||
(nobleHashes as any).randomBytes = customRandomBytes;
|
||||
|
||||
// Sign event
|
||||
console.log('Signing event with patched libraries...');
|
||||
await event.sign();
|
||||
|
||||
// Restore original functions
|
||||
(nobleHashes as any).randomBytes = originalNobleRandomBytes;
|
||||
|
||||
console.log('Event signed successfully');
|
||||
} catch (signError) {
|
||||
console.error('Error signing event:', signError);
|
||||
throw signError;
|
||||
}
|
||||
|
||||
// Publish the event
|
||||
console.log('Publishing event...');
|
||||
await event.publish();
|
||||
|
||||
console.log('Event published successfully:', event.id);
|
||||
return event;
|
||||
} catch (error) {
|
||||
console.error('Error publishing event:', error);
|
||||
console.error('Error details:', error instanceof Error ? error.stack : 'Unknown error');
|
||||
set({ error: error instanceof Error ? error : new Error('Failed to publish event') });
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
fetchEventsByFilter: async (filter: NDKFilter) => {
|
||||
try {
|
||||
const { ndk } = get();
|
||||
|
||||
if (!ndk) {
|
||||
throw new Error('NDK not initialized');
|
||||
}
|
||||
|
||||
// Fetch events
|
||||
const events = await ndk.fetchEvents(filter);
|
||||
|
||||
// Convert Set to Array
|
||||
return Array.from(events);
|
||||
} catch (error) {
|
||||
console.error('Error fetching events:', error);
|
||||
set({ error: error instanceof Error ? error : new Error('Failed to fetch events') });
|
||||
return [];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}));
|
274
package-lock.json
generated
274
package-lock.json
generated
@ -10,8 +10,10 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@expo/cli": "^0.22.16",
|
||||
"@noble/hashes": "^1.7.1",
|
||||
"@noble/secp256k1": "^2.2.3",
|
||||
"@nostr-dev-kit/ndk": "^2.12.0",
|
||||
"@nostr-dev-kit/ndk-mobile": "^0.4.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@react-native-clipboard/clipboard": "^1.16.1",
|
||||
"@react-navigation/material-top-tabs": "^7.1.0",
|
||||
@ -46,9 +48,11 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"expo": "^52.0.35",
|
||||
"expo-crypto": "~14.0.2",
|
||||
"expo-file-system": "~18.0.10",
|
||||
"expo-linking": "~7.0.4",
|
||||
"expo-navigation-bar": "~4.0.8",
|
||||
"expo-random": "^14.0.1",
|
||||
"expo-router": "~4.0.16",
|
||||
"expo-secure-store": "~14.0.1",
|
||||
"expo-splash-screen": "~0.29.20",
|
||||
@ -64,6 +68,7 @@
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.76.7",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-pager-view": "6.5.1",
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
@ -2289,12 +2294,127 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bacons/text-decoder": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@bacons/text-decoder/-/text-decoder-0.0.0.tgz",
|
||||
"integrity": "sha512-8KNbnXSHfhZRR1S1IQEdWQNa9HE/ylWRisDdkoCmHiaP53mksnPaxyqUSlwpJ3DyG1xEekRwFDEG+pbCbSsrkQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cashu/cashu-ts": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-2.2.1.tgz",
|
||||
"integrity": "sha512-/A8Lfkf7nexldcAcTbqrITXxwgiCYTTnrthB8DoipLVeDfyUXer48FJdUmXpRp87Aijn2BNklo8qA0yO0kHXaA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cashu/crypto": "^0.3.4",
|
||||
"@noble/curves": "^1.3.0",
|
||||
"@noble/hashes": "^1.3.3",
|
||||
"buffer": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@cashu/cashu-ts/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cashu/crypto": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.4.tgz",
|
||||
"integrity": "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@noble/curves": "^1.6.0",
|
||||
"@noble/hashes": "^1.5.0",
|
||||
"@scure/bip32": "^1.5.0",
|
||||
"@scure/bip39": "^1.4.0",
|
||||
"buffer": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@cashu/crypto/node_modules/@scure/bip32": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz",
|
||||
"integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@noble/curves": "~1.8.1",
|
||||
"@noble/hashes": "~1.7.1",
|
||||
"@scure/base": "~1.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@cashu/crypto/node_modules/@scure/bip39": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz",
|
||||
"integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@noble/hashes": "~1.7.1",
|
||||
"@scure/base": "~1.2.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@cashu/crypto/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/hammerjs": {
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||
@ -3880,6 +4000,71 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk-mobile": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-mobile/-/ndk-mobile-0.4.1.tgz",
|
||||
"integrity": "sha512-ounoAkMrG5f2Vg+cvlW2x2DZJp4YupcJ4NZkOXumBspxbCbSjZ5B7ILSxo/2adgDwpGniXKQxnxoBVCqk2Lofw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bacons/text-decoder": "^0.0.0",
|
||||
"@nostr-dev-kit/ndk": "2.12.0",
|
||||
"@nostr-dev-kit/ndk-wallet": "0.4.1",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"typescript-lru-cache": "^2.0.0",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"expo-nip55": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk-mobile/node_modules/zustand": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
|
||||
"integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk-wallet": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-wallet/-/ndk-wallet-0.4.1.tgz",
|
||||
"integrity": "sha512-TSKjgxgKAo0PaXgKYUwqWeCV4Q0CioGBTYuWtPY80nkReJMss7TY0RKy+5DrEapwi2eN0OhZuwAyltzF8BDSiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nostr-dev-kit/ndk": "2.12.0",
|
||||
"debug": "^4.3.4",
|
||||
"light-bolt11-decoder": "^3.0.0",
|
||||
"tseep": "^1.1.1",
|
||||
"typescript": "^5.4.4",
|
||||
"webln": "^0.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cashu/cashu-ts": "*",
|
||||
"@cashu/crypto": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@npmcli/fs": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz",
|
||||
@ -8946,6 +9131,15 @@
|
||||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chrome": {
|
||||
"version": "0.0.74",
|
||||
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.74.tgz",
|
||||
"integrity": "sha512-hzosS5CkQcIKCgxcsV2AzbJ36KNxG/Db2YEN/erEu7Boprg+KpMDLBQqKFmSo+JkQMGqRcicUyqCowJpuT+C6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/filesystem": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
@ -8981,6 +9175,21 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/filesystem": {
|
||||
"version": "0.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz",
|
||||
"integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/filewriter": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/filewriter": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz",
|
||||
"integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||
@ -11982,6 +12191,18 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-crypto": {
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-14.0.2.tgz",
|
||||
"integrity": "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-file-system": {
|
||||
"version": "18.0.10",
|
||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.10.tgz",
|
||||
@ -12121,6 +12342,31 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-nip55": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/expo-nip55/-/expo-nip55-0.1.5.tgz",
|
||||
"integrity": "sha512-TLeo7Kne7Yj138av1Zbvyrh/cG9jozXVeS42g8QJ2WdlnB329MzB6wmx7BkYS5x/MXTL+KvpL3AL8XVHHna51A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-random": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/expo-random/-/expo-random-14.0.1.tgz",
|
||||
"integrity": "sha512-gX2mtR9o+WelX21YizXUCD/y+a4ZL+RDthDmFkHxaYbdzjSYTn8u/igoje/l3WEO+/RYspmqUFa8w/ckNbt6Vg==",
|
||||
"deprecated": "This package is now deprecated in favor of expo-crypto, which provides the same functionality. To migrate, replace all imports from expo-random with imports from expo-crypto.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-router": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-4.0.17.tgz",
|
||||
@ -12276,6 +12522,12 @@
|
||||
"type": "^2.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-base64-decode": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz",
|
||||
"integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@ -17663,6 +17915,18 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-get-random-values": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz",
|
||||
"integrity": "sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-base64-decode": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": ">=0.56"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-helmet-async": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-helmet-async/-/react-native-helmet-async-2.0.4.tgz",
|
||||
@ -19804,7 +20068,6 @@
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@ -20233,6 +20496,15 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/webln": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/webln/-/webln-0.3.2.tgz",
|
||||
"integrity": "sha512-YYT83aOCLup2AmqvJdKtdeBTaZpjC6/JDMe8o6x1kbTYWwiwrtWHyO//PAsPixF3jwFsAkj5DmiceB6w/QSe7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chrome": "^0.0.74"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.98.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
|
||||
|
@ -24,8 +24,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/cli": "^0.22.16",
|
||||
"@noble/hashes": "^1.7.1",
|
||||
"@noble/secp256k1": "^2.2.3",
|
||||
"@nostr-dev-kit/ndk": "^2.12.0",
|
||||
"@nostr-dev-kit/ndk-mobile": "^0.4.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@react-native-clipboard/clipboard": "^1.16.1",
|
||||
"@react-navigation/material-top-tabs": "^7.1.0",
|
||||
@ -60,9 +62,11 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"expo": "^52.0.35",
|
||||
"expo-crypto": "~14.0.2",
|
||||
"expo-file-system": "~18.0.10",
|
||||
"expo-linking": "~7.0.4",
|
||||
"expo-navigation-bar": "~4.0.8",
|
||||
"expo-random": "^14.0.1",
|
||||
"expo-router": "~4.0.16",
|
||||
"expo-secure-store": "~14.0.1",
|
||||
"expo-splash-screen": "~0.29.20",
|
||||
@ -78,6 +82,7 @@
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.76.7",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-pager-view": "6.5.1",
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
@ -10,6 +10,7 @@ export interface NostrEvent {
|
||||
}
|
||||
|
||||
export enum NostrEventKind {
|
||||
TEXT = 1,
|
||||
EXERCISE = 33401,
|
||||
TEMPLATE = 33402,
|
||||
WORKOUT = 1301 // Updated from 33403 to 1301
|
||||
|
Loading…
x
Reference in New Issue
Block a user