nostr testing component for publishing workout events

This commit is contained in:
DocNR 2025-03-03 22:40:11 -05:00
parent 173e4e31e4
commit 8e85ee4704
12 changed files with 1289 additions and 500 deletions

View File

@ -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>

View File

@ -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,

View File

@ -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
View 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();

View File

@ -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
};
}

View File

@ -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
View 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
}
}

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -1,6 +1,7 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"moduleResolution": "bundler",
"strict": true,
"baseUrl": ".",
"paths": {

View File

@ -10,6 +10,7 @@ export interface NostrEvent {
}
export enum NostrEventKind {
TEXT = 1,
EXERCISE = 33401,
TEMPLATE = 33402,
WORKOUT = 1301 // Updated from 33403 to 1301