fix: improve authentication state persistence with Zustand

- Standardize secure storage keys across auth systems
- Fix inconsistent key naming in NDK store and auth providers
- Implement proper credential migration between storage systems
- Enhance error handling during credential restoration
- Fix private key authentication not persisting across app restarts
- Add detailed logging for auth initialization sequence
- Improve overall authentication stability with better state management
This commit is contained in:
DocNR 2025-04-05 23:47:12 -04:00
parent c441c5afa5
commit 9ad50956f8
11 changed files with 995 additions and 139 deletions

View File

@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Authentication persistence debugging tools
- Created dedicated AuthPersistenceTest screen for diagnosing credential issues
- Added comprehensive SecureStore key visualization
- Implemented manual key migration triggering
- Added test key creation functionality for simulating scenarios
- Built key clearing utilities for testing from scratch
- Added interactive testing workflow with detailed instructions
- Enhanced error handling with better messaging
- React Query Android Profile Optimization System
- Added platform-specific timeouts for network operations
- Created fallback UI system for handling network delays
@ -17,6 +26,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved user experience during temporary API failures
### Improved
- Authentication initialization sequence
- Added proper awaiting of NDK relay connections
- Implemented credential migration before authentication starts
- Enhanced AuthProvider with improved initialization flow
- Added robustness against race conditions during startup
- Implemented proper detection of stored credentials
- Created key migration system between storage locations
- Enhanced app_layout.tsx to ensure proper initialization order
- Added detailed technical documentation for the auth system
- Profile loading performance dramatically enhanced
- Added ultra-early content display after just 500ms
- Implemented progressive content loading with three-tier system
@ -48,6 +67,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Created comprehensive logging documentation
### Fixed
- Authentication storage key inconsistencies
- Fixed inconsistent key naming between different auth systems
- Implemented consistent SECURE_STORE_KEYS constants
- Created migration utility in secureStorage.ts
- Added key migration from legacy to standardized locations
- Fixed AuthProvider and AuthStateManager to use the same keys
- Enhanced NDK store to use standardized key constants
- Added migration status tracking to prevent duplicate migrations
- Created diagnostic tool for checking credential storage
- Fixed ReactQueryAuthProvider to use the same key constants
- Added detailed documentation in authentication_persistence_debug_guide.md
- Private key authentication persistence
- Fixed inconsistent storage key naming between legacy and React Query auth systems
- Standardized on 'nostr_privkey' for all private key storage
@ -82,6 +113,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added comprehensive logging for better debugging
- Fixed race conditions in authentication state transitions
- Implemented initialization tracking to prevent duplicate auth operations
- Added waiting for NDK pool initialization before auth operations
- Created one-time migration system for legacy credentials
- Fixed delayed authentication restoration with improved sequence checks
- Enhanced credential consistency verification at startup
- Added test tools for diagnosing and fixing authentication issues
- Android profile screen hanging issues
- Fixed infinite loading state on profile screen with proper timeouts
@ -698,51 +734,4 @@ g
- Added type safety for complex operations
- Improved error handling throughout relay management
# Changelog - March 8, 2025
## Added
- Database schema upgrade to version 5
- Added workouts, workout_exercises, and workout_sets tables
- Added templates and template_exercises tables
- Added publication_queue table for offline-first functionality
- Added app_status table for connectivity tracking
- New database services
- WorkoutService for managing workout data persistence
- Enhanced TemplateService for template management
- NostrWorkoutService for Nostr event conversion
- Updated PublicationQueueService for offline publishing
- React hooks for database access
- useWorkouts hook for workout operations
- useTemplates hook for template operations
- Improved workout completion flow
- Three-tier storage approach (Local Only, Publish Complete, Publish Limited)
- Template modification options (keep original, update, save as new)
- Enhanced social sharing capabilities
- Detailed workout summary with statistics
- Enhanced database debugging tools
- Added proper error handling and logging
- Improved transaction management
- Added connectivity status tracking
## Fixed
- Missing workout and template table errors
- Incomplete data storage issues
- Template management synchronization
- Nostr event conversion between app models and Nostr protocol
- Workout persistence across app sessions
- Database transaction handling in workout operations
- Template reference handling in workout records
## Improved
- Workout store persistence layer
- Enhanced integration with database services
- Better error handling for database operations
- Improved Nostr connectivity detection
- Template management workflow
- Proper versioning and attribution
- Enhanced modification tracking
- Better user control over template sharing
- Overall data persistence architecture
- Consistent service-based approach
- Improved type safety
- Enhanced error
# Changelog - March 8,

View File

@ -23,6 +23,8 @@ import { useWorkoutStore } from '@/stores/workoutStore';
import { ConnectivityService } from '@/lib/db/services/ConnectivityService';
import { AuthProvider } from '@/lib/auth/AuthProvider';
import { ReactQueryAuthProvider } from '@/lib/auth/ReactQueryAuthProvider';
import { QueryClientProvider } from '@tanstack/react-query';
import { createQueryClient } from '@/lib/queryClient';
// Import splash screens with improved fallback mechanism
let SplashComponent: React.ComponentType<{onFinish: () => void}>;
@ -115,6 +117,8 @@ export default function RootLayout() {
const { colorScheme, isDarkColorScheme } = useColorScheme();
const { init } = useNDKStore();
const initializationPromise = React.useRef<Promise<void> | null>(null);
// Create a query client instance that can be used by both auth systems
const queryClient = React.useMemo(() => createQueryClient(), []);
// Start app initialization immediately
React.useEffect(() => {
@ -135,12 +139,23 @@ export default function RootLayout() {
// Start database initialization and NDK initialization in parallel
const initPromises = [];
// Initialize NDK with timeout
const ndkPromise = init().catch(error => {
console.error('NDK initialization error:', error);
// Continue even if NDK fails
return { offlineMode: true };
});
// Initialize NDK with credentials migration first
const ndkPromise = (async () => {
try {
// Import and run key migration before NDK init
const { migrateKeysIfNeeded } = await import('@/lib/auth/persistence/secureStorage');
console.log('Running pre-NDK credential migration...');
await migrateKeysIfNeeded();
// Now initialize NDK
console.log('Starting NDK initialization...');
return await init();
} catch (error) {
console.error('NDK initialization error:', error);
// Continue even if NDK fails
return { offlineMode: true };
}
})();
initPromises.push(ndkPromise);
// Load favorites from SQLite (local operation)
@ -232,7 +247,7 @@ export default function RootLayout() {
{/* Conditionally render authentication providers based on feature flag */}
{FLAGS.useReactQueryAuth ? (
/* Use React Query Auth system */
<ReactQueryAuthProvider enableNDK={true}>
<ReactQueryAuthProvider enableNDK={true} queryClient={queryClient}>
{/* React Query specific components */}
<RelayInitializer reactQueryMode={true} />
<OfflineIndicator />
@ -253,26 +268,30 @@ export default function RootLayout() {
<PortalHost />
</ReactQueryAuthProvider>
) : (
/* Use Legacy Auth system */
<AuthProvider ndk={useNDKStore.getState().ndk!}>
<RelayInitializer reactQueryMode={false} />
<OfflineIndicator />
<StatusBar style={isDarkColorScheme ? 'light' : 'dark'} />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen
name="(tabs)"
options={{
headerShown: false,
}}
/>
</Stack>
{/* Settings drawer needs to be outside the navigation stack */}
<SettingsDrawer />
<PortalHost />
</AuthProvider>
/* Use Legacy Auth system but still provide QueryClientProvider and NDKContext for data fetching */
<QueryClientProvider client={queryClient}>
<NDKContext.Provider value={{ ndk: useNDKStore.getState().ndk, isInitialized: true }}>
<AuthProvider ndk={useNDKStore.getState().ndk!}>
<RelayInitializer reactQueryMode={false} />
<OfflineIndicator />
<StatusBar style={isDarkColorScheme ? 'light' : 'dark'} />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen
name="(tabs)"
options={{
headerShown: false,
}}
/>
</Stack>
{/* Settings drawer needs to be outside the navigation stack */}
<SettingsDrawer />
<PortalHost />
</AuthProvider>
</NDKContext.Provider>
</QueryClientProvider>
)}
</SettingsDrawerProvider>
</ThemeProvider>

View File

@ -0,0 +1,271 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Button, ScrollView, Platform, TouchableOpacity } from 'react-native';
import { Stack, useRouter } from 'expo-router';
import { X } from 'lucide-react-native';
import * as SecureStore from 'expo-secure-store';
import { SECURE_STORE_KEYS } from '@/lib/auth/constants';
import { useAuthStore } from '@/lib/auth/AuthStateManager';
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
import { migrateKeysIfNeeded, resetMigration } from '@/lib/auth/persistence/secureStorage';
/**
* Test screen for debugging authentication persistence issues
* This component shows the current state of authentication and stored credentials
*/
export default function AuthPersistenceTest() {
const [storageInfo, setStorageInfo] = useState({
standardPrivateKey: 'Checking...',
legacyPrivateKey: 'Checking...',
ndkStoreKey: 'Checking...',
pubkey: 'Checking...',
externalSigner: 'Checking...',
migrationStatus: 'Checking...'
});
// Get auth states from both stores (legacy and React Query)
const authState = useAuthStore((state) => state);
const { isAuthenticated, currentUser } = useNDKCurrentUser();
const router = useRouter();
const checkStorage = async () => {
try {
// Check all possible storage keys
const standardPrivateKey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
const legacyPrivateKey = await SecureStore.getItemAsync('powr.private_key');
const ndkStoreKey = await SecureStore.getItemAsync('nostr_privkey');
const pubkey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PUBKEY);
const externalSigner = await SecureStore.getItemAsync(SECURE_STORE_KEYS.EXTERNAL_SIGNER);
const migrationStatus = await SecureStore.getItemAsync('auth_migration_v1_completed');
// Update state with results
setStorageInfo({
standardPrivateKey: standardPrivateKey
? `Found (${standardPrivateKey.length} chars)`
: 'Not found',
legacyPrivateKey: legacyPrivateKey
? `Found (${legacyPrivateKey.length} chars)`
: 'Not found',
ndkStoreKey: ndkStoreKey
? `Found (${ndkStoreKey.length} chars)`
: 'Not found',
pubkey: pubkey
? `Found (${pubkey.substring(0, 8)}...)`
: 'Not found',
externalSigner: externalSigner
? 'Found'
: 'Not found',
migrationStatus: migrationStatus === 'true'
? 'Completed'
: 'Not run'
});
} catch (error: any) {
const errorMsg = error?.message || 'Unknown error';
console.error('Error checking storage:', errorMsg);
setStorageInfo({
standardPrivateKey: `Error: ${errorMsg}`,
legacyPrivateKey: `Error: ${errorMsg}`,
ndkStoreKey: `Error: ${errorMsg}`,
pubkey: `Error: ${errorMsg}`,
externalSigner: `Error: ${errorMsg}`,
migrationStatus: `Error: ${errorMsg}`
});
}
};
// Check storage when component mounts
useEffect(() => {
checkStorage();
}, []);
return (
<>
<Stack.Screen
options={{
title: 'Auth Persistence Test',
headerLeft: () => Platform.OS === 'ios' ? (
<TouchableOpacity onPress={() => router.back()} style={{ marginLeft: 15 }}>
<X size={24} color="#000" />
</TouchableOpacity>
) : undefined,
headerRight: () => Platform.OS !== 'ios' ? (
<TouchableOpacity onPress={() => router.back()} style={{ marginRight: 15 }}>
<X size={24} color="#000" />
</TouchableOpacity>
) : undefined,
}}
/>
<ScrollView style={styles.container}>
<Text style={styles.title}>Auth Persistence Debugger</Text>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Zustand Auth State</Text>
<Text>Status: {authState.status}</Text>
{authState.status === 'authenticated' && (
<>
<Text>User: {authState.user?.pubkey?.substring(0, 8)}...</Text>
<Text>Method: {authState.method || 'Unknown'}</Text>
</>
)}
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>React Query Auth State</Text>
<Text>Status: {isAuthenticated ? 'authenticated' : 'unauthenticated'}</Text>
{isAuthenticated && currentUser?.pubkey && (
<Text>User: {currentUser.pubkey.substring(0, 8)}...</Text>
)}
<Text>User Profile: {currentUser?.profile?.name || 'Not loaded'}</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Secure Storage Status</Text>
<Text>Standard Key ({SECURE_STORE_KEYS.PRIVATE_KEY}):</Text>
<Text style={styles.value}>{storageInfo.standardPrivateKey}</Text>
<Text>Legacy Key (powr.private_key):</Text>
<Text style={styles.value}>{storageInfo.legacyPrivateKey}</Text>
<Text>NDK Store Key (nostr_privkey):</Text>
<Text style={styles.value}>{storageInfo.ndkStoreKey}</Text>
<Text>Public Key:</Text>
<Text style={styles.value}>{storageInfo.pubkey}</Text>
<Text>External Signer:</Text>
<Text style={styles.value}>{storageInfo.externalSigner}</Text>
<Text>Migration Status:</Text>
<Text style={styles.value}>{storageInfo.migrationStatus}</Text>
</View>
<View style={styles.buttonContainer}>
<Button
title="Refresh Storage Info"
onPress={checkStorage}
/>
<View style={{ height: 10 }} />
<Button
title="Run Key Migration Manually"
onPress={async () => {
try {
await migrateKeysIfNeeded();
console.log('Migration completed successfully');
checkStorage();
} catch (e) {
console.error('Migration failed:', e);
}
}}
/>
<View style={{ height: 10 }} />
<Button
title="Reset Migration Status (DEV)"
onPress={async () => {
try {
await resetMigration();
console.log('Migration status reset');
checkStorage();
} catch (e) {
console.error('Reset failed:', e);
}
}}
/>
<View style={{ height: 10 }} />
<Button
title="Create Test Keys"
color="#4CAF50"
onPress={async () => {
try {
// Generate a simple test key
const testKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
// Store in legacy location for testing migration
await SecureStore.setItemAsync('powr.private_key', testKey);
console.log('Test key created in legacy location');
checkStorage();
} catch (e) {
console.error('Failed to create test key:', e);
}
}}
/>
<View style={{ height: 10 }} />
<Button
title="Clear All Auth Keys"
color="#F44336"
onPress={async () => {
try {
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.PUBKEY);
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.EXTERNAL_SIGNER);
await SecureStore.deleteItemAsync('powr.private_key');
await SecureStore.deleteItemAsync('nostr_privkey');
await SecureStore.deleteItemAsync('auth_migration_v1_completed');
console.log('All keys cleared');
checkStorage();
} catch (e) {
console.error('Failed to clear keys:', e);
}
}}
/>
</View>
<View style={styles.instructions}>
<Text style={styles.sectionTitle}>Testing Instructions</Text>
<Text>1. Click "Clear All Auth Keys" to start fresh</Text>
<Text>2. Click "Create Test Keys" to add a key in the legacy location</Text>
<Text>3. Force close and restart the app</Text>
<Text>4. Return to this screen and check if auth persisted</Text>
<Text>5. Check "Migration Status" to see if keys were migrated</Text>
</View>
</ScrollView>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
section: {
backgroundColor: 'white',
padding: 16,
borderRadius: 8,
marginBottom: 16,
borderWidth: 1,
borderColor: '#e0e0e0',
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
value: {
marginBottom: 8,
paddingLeft: 10,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
},
buttonContainer: {
marginBottom: 20,
},
instructions: {
backgroundColor: '#e8f5e9',
padding: 16,
borderRadius: 8,
marginBottom: 30,
}
});

View File

@ -0,0 +1,141 @@
// components/AuthDebugScreen.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import { SECURE_STORE_KEYS } from '@/lib/auth/constants';
import { useAuthStore } from '@/lib/auth/AuthStateManager';
export default function AuthDebugScreen() {
const [storageInfo, setStorageInfo] = useState({
privateKey: 'Checking...',
pubkey: 'Checking...',
externalSigner: 'Checking...'
});
const authState = useAuthStore((state) => state);
const checkStorage = async () => {
try {
// Check using the defined constants
const privateKey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
const pubkey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PUBKEY);
const externalSigner = await SecureStore.getItemAsync(SECURE_STORE_KEYS.EXTERNAL_SIGNER);
// Also check the legacy key
const legacyKey = await SecureStore.getItemAsync('powr.private_key');
setStorageInfo({
privateKey: privateKey
? `Found (${privateKey.length} chars)${legacyKey ? ' [also in legacy]' : ''}`
: 'Not found',
pubkey: pubkey
? `Found (${pubkey.substring(0, 8)}...)`
: 'Not found',
externalSigner: externalSigner
? 'Found'
: 'Not found'
});
} catch (error: any) {
console.error('Error checking storage:', error);
const errorMessage = error && error.message ? error.message : 'Unknown error';
setStorageInfo({
privateKey: `Error: ${errorMessage}`,
pubkey: `Error: ${errorMessage}`,
externalSigner: `Error: ${errorMessage}`
});
}
};
useEffect(() => {
checkStorage();
}, []);
return (
<View style={styles.container}>
<Text style={styles.title}>Authentication Debug</Text>
<View style={styles.stateContainer}>
<Text style={styles.sectionTitle}>Auth State</Text>
<Text>Status: {authState.status}</Text>
{authState.status === 'authenticated' && (
<>
<Text>User: {authState.user.pubkey.substring(0, 8)}...</Text>
<Text>Method: {authState.method}</Text>
</>
)}
</View>
<View style={styles.storageContainer}>
<Text style={styles.sectionTitle}>Secure Storage</Text>
<Text>Private Key: {storageInfo.privateKey}</Text>
<Text>Public Key: {storageInfo.pubkey}</Text>
<Text>External Signer: {storageInfo.externalSigner}</Text>
</View>
<Button title="Refresh Storage Info" onPress={checkStorage} />
<View style={styles.actionsContainer}>
<Text style={styles.sectionTitle}>Debug Actions</Text>
<Button
title="Move Keys from Legacy to Current"
onPress={async () => {
const legacyKey = await SecureStore.getItemAsync('powr.private_key');
if (legacyKey) {
await SecureStore.setItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY, legacyKey);
console.log('Moved key from legacy to current');
checkStorage();
} else {
console.log('No legacy key found');
}
}}
/>
<Button
title="Clear All Keys"
onPress={async () => {
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.PUBKEY);
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.EXTERNAL_SIGNER);
await SecureStore.deleteItemAsync('powr.private_key');
console.log('Cleared all keys');
checkStorage();
}}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
title: {
fontSize: 22,
fontWeight: 'bold',
marginBottom: 16,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 8,
},
stateContainer: {
padding: 12,
backgroundColor: '#f0f0f0',
borderRadius: 8,
marginBottom: 16,
},
storageContainer: {
padding: 12,
backgroundColor: '#f0f0f0',
borderRadius: 8,
marginBottom: 16,
},
actionsContainer: {
padding: 12,
backgroundColor: '#f0f0f0',
borderRadius: 8,
marginTop: 16,
gap: 8,
}
});

View File

@ -209,6 +209,12 @@ export default function SettingsDrawer() {
/>
),
},
{
id: 'developer-tools-header',
icon: () => null,
label: '',
onPress: () => {},
},
{
id: 'react-query-demo',
icon: RefreshCw,
@ -220,6 +226,17 @@ export default function SettingsDrawer() {
});
},
},
{
id: 'auth-debug',
icon: Database,
label: 'Auth Debug Tools',
onPress: () => {
closeDrawer();
router.push({
pathname: "/test/auth-persistence-test" as any
});
},
},
{
id: 'relays',
icon: Globe,

View File

@ -1,78 +1,98 @@
# Authentication Persistence Fixes
# Authentication System Fixes Summary
This document summarizes the improvements made to fix authentication persistence issues in the POWR app.
## Authentication Persistence Fix
## Overview
We have successfully implemented a solution for authentication persistence across app restarts. The key components of the fix:
The authentication system was enhanced to ensure reliable persistence of user credentials across app restarts. Previously, users needed to re-authenticate each time they restarted the app, even though their credentials were being stored in SecureStore.
1. **Key Storage Standardization**
- Implemented consistent use of key names through `SECURE_STORE_KEYS` constants
- Set up migration utilities to handle legacy storage formats
- Ensured consistent key storage across different parts of the app
## Key Improvements
2. **Initialization Sequence Improvement**
- Added proper pre-NDK credential migration in _layout.tsx
- Ensured migration happens before NDK initialization
- Fixed race conditions during startup
### 1. Enhanced AuthService
3. **Diagnostic Tools**
- Created AuthPersistenceTest component for real-time debugging
- Added visualization of stored credentials
- Implemented test utilities for manual testing
- **Improved initialization process**: Added promise caching for concurrent calls to prevent race conditions
- **More robust credential restoration**: Better error handling when restoring stored private keys or external signers
- **SecureStore key constants**: Added constants for all SecureStore keys to avoid inconsistencies
- **Public key caching**: Added storage of public key alongside private key for faster access
- **Clean credential handling**: Automatic cleanup of invalid credentials when restoration fails
- **Enhanced logging**: Comprehensive logging throughout the authentication flow
## React Query Integration Fix
### 2. Improved ReactQueryAuthProvider
We also fixed an issue where the app was encountering errors when using the legacy Zustand-based authentication with React Query components. The solution:
- **Better NDK initialization**: Enhanced initialization with credential pre-checking
- **Initialization tracking**: Added tracking of initialization attempts to prevent duplicates
- **State management**: Improved state updates to ensure they only occur for the most recent initialization
- **Auth state invalidation**: Force refresh of auth state after initialization
- **Error resilience**: Better error handling throughout the provider
1. **QueryClientProvider with Dual Auth Systems**
- Added QueryClientProvider to the legacy auth path in _layout.tsx
- Created a shared queryClient instance that works with both auth systems
- Ensured NDKContext is properly provided in both paths
### 3. Fixed useAuthQuery Hook
2. **Component-level Compatibility**
- Components like UserAvatar that use React Query hooks now work in both auth modes
- Data fetching via React Query continues to work even when using Zustand for auth state
- **Pre-initialization**: Added pre-initialization of auth service when the hook is first used
- **Query configuration**: Adjusted query settings for better reliability (staleTime, refetchOnWindowFocus, etc.)
- **Type safety**: Fixed TypeScript errors to ensure proper type checking
- **Initialization reference**: Added reference tracking to prevent memory leaks
- **Better error handling**: Enhanced error management throughout the hook
## Code Changes
## Implementation Details
### 1. Updated app/_layout.tsx to use QueryClientProvider in both auth paths
### AuthService Changes
```tsx
// Create a shared queryClient for both auth systems
const queryClient = React.useMemo(() => createQueryClient(), []);
- Added promise caching with `initPromise` to handle concurrent initialization calls
- Split initialization into public `initialize()` and private `_doInitialize()` methods
- Improved error handling with try/catch blocks and proper cleanup
- Added constants for secure storage keys (`POWR.PRIVATE_KEY`, etc.)
- Enhanced login methods with optional `saveKey` parameter
- Added public key storage for faster reference
// In the render method:
{FLAGS.useReactQueryAuth ? (
// React Query Auth system (already had QueryClientProvider internally)
<ReactQueryAuthProvider enableNDK={true} queryClient={queryClient}>
{/* ... */}
</ReactQueryAuthProvider>
) : (
// Legacy Auth system with added QueryClientProvider and NDKContext
<QueryClientProvider client={queryClient}>
<NDKContext.Provider value={{ ndk: useNDKStore.getState().ndk, isInitialized: true }}>
<AuthProvider ndk={useNDKStore.getState().ndk!}>
{/* ... */}
</AuthProvider>
</NDKContext.Provider>
</QueryClientProvider>
)}
```
### ReactQueryAuthProvider Changes
### 2. Updated ReactQueryAuthProvider to accept a queryClient prop
- Added initialization attempt tracking to prevent race conditions
- Added pre-checking of credentials before NDK initialization
- Enhanced state management to only update for the most recent initialization attempt
- Improved error resilience to ensure app works even if initialization fails
- Added forced query invalidation after successful initialization
The ReactQueryAuthProvider component was enhanced to accept an external queryClient to avoid creating multiple instances.
### useAuthQuery Changes
## Best Practices for Future Development
- Added initialization state tracking with useRef
- Pre-initializes auth service when hook is first used
- Improved mutation error handling with better cleanup
- Fixed TypeScript errors with proper type assertions
- Enhanced query settings for better reliability and performance
When working with dual authentication systems:
## Testing
1. **Data Fetching vs. Auth State Management**
- Keep React Query for data fetching regardless of which auth system is used
- Authentication state can be managed by either Zustand or React Query
2. **Context Providers**
- Always include QueryClientProvider for React Query hooks to work
- Provide NDKContext when not using ReactQueryAuthProvider
- Component hooks that need React Query should always be wrapped in QueryClientProvider
The authentication persistence has been thoroughly tested across various scenarios:
3. **Storage Keys**
- Always reference storage keys from the constants file
- Migrate legacy keys when encountered
- Document key structure and migration paths
1. App restart with private key authentication
2. App restart with external signer (Amber) authentication
3. Logout and login within the same session
4. Network disconnection and reconnection scenarios
5. Force quit and restart of the application
## Testing Tips
## Future Considerations
To test authentication persistence:
- Consider adding a periodic auth token refresh mechanism
- Explore background token validation to prevent session timeout
- Implement token expiration handling if needed in the future
- Potentially add multi-account support with secure credential switching
1. Clear all security keys using the AuthPersistenceTest screen
2. Create test keys and restart the app
3. Verify credentials load correctly on restart
4. Verify proper NDK initialization with loaded credentials
5. Check logs for proper initialization sequence
6. Toggle between auth systems to test compatibility of both approaches
## Known Limitations
- Legacy components that depend on React Query still need the QueryClientProvider wrapper
- Components must handle the possibility that NDK might not be initialized yet
- External signers require separate handling with dedicated hooks

View File

@ -0,0 +1,137 @@
# Authentication Persistence Debugging Guide
## Overview
This guide explains the authentication persistence system in the POWR app and how to debug issues related to user authentication state not being properly restored after app restarts.
## Background
The app uses several authentication mechanisms:
1. **Zustand-based Auth System** - The default authentication system using a state machine pattern
2. **React Query-based Auth System** - Alternative auth system toggled via feature flag
3. **XState Auth System** - Partially implemented auth system (not fully deployed)
## Authentication Storage
Credentials are stored securely using `expo-secure-store` with the following key architecture:
| Key Name | Storage Location | Description |
|----------|-----------------|-------------|
| `nostr_privkey` | Standard Key | The primary location for private keys (from constants.ts) |
| `nostr_pubkey` | Standard Key | User's public key |
| `nostr_external_signer` | Standard Key | External signer configuration (e.g., Amber) |
| `powr.private_key` | Legacy Key | Legacy location for private keys |
## Common Issues
### 1. Inconsistent Storage Keys
One of the most common issues was inconsistent storage key usage across different parts of the app. This has been fixed by:
- Centralizing key definitions in `lib/auth/constants.ts`
- Creating a migration utility in `lib/auth/persistence/secureStorage.ts`
- Ensuring all parts of the auth system use the same keys
### 2. Race Conditions During Init
Another common issue was race conditions during app initialization:
- NDK initialization happening before credentials are loaded
- Auth state checks happening before provider initialization completes
- Multiple competing auth systems trying to initialize simultaneously
## Implemented Solutions
We've implemented the following solutions to address these issues:
1. **Storage Key Migration**
- The app now automatically migrates keys from legacy locations to the standardized ones
- This happens automatically during app initialization
- The migration occurs only once and tracks its completion state
2. **Improved Initialization Sequence**
- Added proper sequencing in `app/_layout.tsx`
- Key migration now runs before NDK initialization
- Auth providers wait for NDK to be ready before initializing
- Implemented proper waiting for relay connections
3. **AuthProvider Enhancements**
- Enhanced error handling and logging
- Added state inconsistency detection and recovery
- Improved external signer handling
## Debugging Tools
### Auth Persistence Test Screen
A dedicated test screen is available at `app/test/auth-persistence-test.tsx` for debugging auth persistence issues. It provides:
- Current auth state visualization
- SecureStore key inspection
- Manual migration triggers
- Test key creation for simulating scenarios
- Key clearing functionality
### How to Debug Persistence Issues
If a user reports auth persistence problems:
1. Check if credentials exist in any storage location
- Use the Auth Persistence Test screen
- Check both standard and legacy locations
2. Verify migration status
- The `auth_migration_v1_completed` key indicates if migration has run
- If not, you can trigger it manually from the test screen
3. Test with a fresh key
- Clear all keys
- Create a test key in the legacy location
- Force restart the app
- Check if migration and auth restoration work properly
4. Check logs for initialization sequence
- Look for `[Auth]` and `[AuthProvider]` prefixed logs
- Verify that key migration runs before NDK initialization
- Ensure there are no initialization errors
## Implementation Details
### Key Migration Utility
The key migration utility in `lib/auth/persistence/secureStorage.ts` handles:
- One-time migration from legacy to standard locations
- Migration status tracking
- Graceful handling of missing keys
- Migration priority rules (preserve existing keys)
### Configuration
Feature flags in `lib/stores/ndk.ts` control which auth system is active:
```typescript
export const FLAGS = {
useReactQueryAuth: false, // When true, use React Query auth; when false, use legacy auth
};
```
## Troubleshooting Steps
If a user's authentication is not persisting:
1. **Check Storage Keys**: Use the Auth Persistence Test screen to see if credentials exist
2. **Run Migration**: Try manual migration if storage shows keys in legacy locations
3. **Test Restart Flow**: Clear keys, create test keys, and force restart the app
4. **Toggle Auth Systems**: As a last resort, try toggling the feature flag to use the other auth system
5. **Clear App Data**: If all else fails, have the user clear app data and re-authenticate
## Future Improvements
Planned improvements to the auth persistence system:
1. Phasing out the legacy key locations completely
2. Further unifying the auth providers into a single consistent system
3. Adding more robust error recovery mechanisms
4. Improving the initialization sequence to be more resilient to timing issues

View File

@ -1,7 +1,11 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
import { useAuthStore } from './AuthStateManager';
import { AuthService } from './AuthService';
import NDK from '@nostr-dev-kit/ndk-mobile';
import * as SecureStore from 'expo-secure-store';
import { SECURE_STORE_KEYS } from './constants';
import { migrateKeysIfNeeded } from './persistence/secureStorage';
import { Platform } from 'react-native';
/**
* Context value interface for the Auth context
@ -25,29 +29,148 @@ interface AuthProviderProps {
/**
* Provider component that makes auth service available to the app
* Fixed with proper initialization sequence and credential handling
*/
export const AuthProvider: React.FC<AuthProviderProps> = ({ children, ndk }) => {
// Create a singleton instance of AuthService
const [authService] = useState(() => new AuthService(ndk));
// Subscribe to auth state for debugging/monitoring purposes
const authState = useAuthStore();
// Track initialization to prevent duplicate attempts
const initializingRef = useRef(false);
// Initialize auth on mount
// Initialize auth on mount with improved sequence
useEffect(() => {
const initAuth = async () => {
// Prevent multiple init attempts
if (initializingRef.current) {
console.log("[AuthProvider] Already initializing, skipping");
return;
}
initializingRef.current = true;
try {
console.log("[AuthProvider] Initializing authentication");
console.log(`[AuthProvider] Initializing authentication (${Platform.OS})`);
// First, ensure NDK is fully connected before proceeding
if (!ndk) {
console.error("[AuthProvider] NDK is null, cannot initialize auth");
return;
}
// Wait for NDK to be connected before proceeding
if (!ndk.pool) {
console.log("[AuthProvider] Waiting for NDK to initialize pool...");
let attempts = 0;
const maxAttempts = 20; // 2 seconds max wait with 100ms checks
await new Promise<void>((resolve) => {
const checkInterval = setInterval(() => {
attempts++;
// Check if NDK has been connected
if (ndk.pool) {
clearInterval(checkInterval);
console.log("[AuthProvider] NDK pool initialized, proceeding with auth");
resolve();
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval);
console.warn("[AuthProvider] Timed out waiting for relay connections, proceeding anyway");
resolve();
}
}, 100);
});
}
// Run key migration to ensure credentials are in the correct location
await migrateKeysIfNeeded();
// Initialize the auth service
await authService.initialize();
console.log("[AuthProvider] Authentication initialized");
console.log("[AuthProvider] Authentication service initialized");
// Verify auth state is consistent with stored credentials
const privateKey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
const externalSigner = await SecureStore.getItemAsync(SECURE_STORE_KEYS.EXTERNAL_SIGNER);
const currentState = useAuthStore.getState();
// If we have credentials but auth state is unauthenticated, attempt to restore
if ((privateKey || externalSigner) && currentState.status === 'unauthenticated') {
console.log("[AuthProvider] Auth state inconsistent with storage, attempting restore");
if (privateKey) {
try {
// DIRECT APPROACH: Create a signer with the private key
console.log("[AuthProvider] Restoring auth with stored private key");
// Import NDKPrivateKeySigner for direct key operations
const { NDKPrivateKeySigner } = await import('@nostr-dev-kit/ndk-mobile');
// Create a signer directly
const signer = new NDKPrivateKeySigner(privateKey);
ndk.signer = signer;
// Connect to establish NDK user
await ndk.connect();
// After connecting, get the public key directly from the user object
const publicKey = ndk.activeUser?.pubkey;
if (!publicKey) {
throw new Error("Failed to get public key from NDK");
}
console.log(`[AuthProvider] Retrieved public key: ${publicKey.substring(0, 8)}...`);
// Store the public key for future quick access
await SecureStore.setItemAsync(SECURE_STORE_KEYS.PUBKEY, publicKey);
console.log("[AuthProvider] Saved public key to SecureStore");
if (ndk.activeUser) {
console.log("[AuthProvider] NDK authenticated successfully, updating Zustand store");
// CRITICAL: Update the Zustand store directly
const authStore = useAuthStore.getState();
authStore.setAuthenticated(ndk.activeUser, 'private_key');
console.log("[AuthProvider] Zustand store updated with authenticated state");
} else {
console.error("[AuthProvider] Failed to set NDK activeUser");
}
} catch (err) {
console.error("[AuthProvider] Failed to restore with private key:", err);
}
} else if (externalSigner) {
console.log("[AuthProvider] External signer credentials found, re-initializing auth service");
// Just re-initialize the auth service - it will handle the external signer restoration internally
try {
await authService.initialize();
// Ensure the public key is stored for external signer too
if (ndk.activeUser) {
const publicKey = ndk.activeUser.pubkey;
await SecureStore.setItemAsync(SECURE_STORE_KEYS.PUBKEY, publicKey);
console.log("[AuthProvider] Saved external signer public key to SecureStore");
// Update Zustand store directly for consistent auth state
const authStore = useAuthStore.getState();
authStore.setAuthenticated(ndk.activeUser, 'amber');
}
} catch (err) {
console.error("[AuthProvider] Failed to restore with external signer:", err);
}
}
}
} catch (error) {
console.error("[AuthProvider] Error initializing authentication:", error);
} finally {
initializingRef.current = false;
}
};
initAuth();
// No cleanup needed - AuthService instance persists for app lifetime
}, [authService]);
}, [ndk, authService]);
// Debugging: Log auth state changes
useEffect(() => {

View File

@ -7,8 +7,9 @@ import {
AuthMethod,
SigningOperation
} from "./types";
import { SECURE_STORE_KEYS } from "./constants";
const PRIVATE_KEY_STORAGE_KEY = "powr.private_key";
const PRIVATE_KEY_STORAGE_KEY = SECURE_STORE_KEYS.PRIVATE_KEY; // Use the constant from constants.ts
/**
* Zustand store that manages the authentication state

View File

@ -0,0 +1,136 @@
/**
* Secure storage utilities for authentication
* Handles credential storage, retrieval, and migration between key formats
*/
import * as SecureStore from 'expo-secure-store';
import { SECURE_STORE_KEYS } from '../constants';
import { Platform } from 'react-native';
// Unique key to track migration status
const MIGRATION_VERSION_KEY = 'auth_migration_v1_completed';
// Legacy storage keys that might contain credentials
const LEGACY_KEYS = {
PRIVATE_KEY: 'powr.private_key',
NOSTR_PRIVKEY: 'nostr_privkey', // Original key from ndk.ts
};
/**
* Migrates credentials from legacy storage keys to the standardized keys
* This is a one-time operation that runs at app startup
*
* @returns Promise that resolves when migration is complete
*/
export async function migrateKeysIfNeeded(): Promise<void> {
console.log(`[Auth] Starting key migration check (${Platform.OS})`);
// Check if migration already happened
const migrationDone = await SecureStore.getItemAsync(MIGRATION_VERSION_KEY);
if (migrationDone === 'true') {
console.log('[Auth] Migration already completed, skipping');
return;
}
console.log('[Auth] Performing one-time key migration');
// Get the current value from the standardized location (if any)
const currentKey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
if (currentKey) {
console.log('[Auth] Already have credentials in standard location');
}
// Check for credentials in legacy locations
const legacyKey = await SecureStore.getItemAsync(LEGACY_KEYS.PRIVATE_KEY);
const ndkStoreKey = await SecureStore.getItemAsync(LEGACY_KEYS.NOSTR_PRIVKEY);
console.log('[Auth] Storage key status:', {
hasCurrentKey: !!currentKey,
hasLegacyKey: !!legacyKey,
hasNdkStoreKey: !!ndkStoreKey
});
// Migration strategy: prioritize existing credentials if any
if (!currentKey) {
if (legacyKey) {
console.log('[Auth] Found credentials in legacy location (powr.private_key), migrating');
await SecureStore.setItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY, legacyKey);
console.log('[Auth] Legacy key (powr.private_key) migrated successfully');
} else if (ndkStoreKey) {
console.log('[Auth] Found credentials in ndk store location (nostr_privkey), migrating');
await SecureStore.setItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY, ndkStoreKey);
console.log('[Auth] NDK store key (nostr_privkey) migrated successfully');
}
}
// Mark migration as complete regardless of outcome
// This prevents repeated migration attempts
await SecureStore.setItemAsync(MIGRATION_VERSION_KEY, 'true');
console.log('[Auth] Key migration process completed');
}
/**
* Clear all authentication credentials from secure storage
* Handles both current and legacy keys
*/
export async function clearAllCredentials(): Promise<void> {
console.log('[Auth] Clearing all stored credentials');
// Define all keys that might contain credentials
const allKeys = [
SECURE_STORE_KEYS.PRIVATE_KEY,
SECURE_STORE_KEYS.EXTERNAL_SIGNER,
SECURE_STORE_KEYS.PUBKEY,
LEGACY_KEYS.PRIVATE_KEY,
LEGACY_KEYS.NOSTR_PRIVKEY,
];
// Delete all possible keys
await Promise.all(allKeys.map(key => {
console.log(`[Auth] Deleting key: ${key}`);
return SecureStore.deleteItemAsync(key);
}));
console.log('[Auth] All credentials cleared');
}
/**
* Gets a credential from secure storage with fallback to legacy locations
* Useful during transition period when old keys might still be in use
*
* @param key The main storage key to check
* @param legacyKeys Array of legacy keys to check as fallbacks
* @returns The credential value if found, null otherwise
*/
export async function getCredentialWithFallback(
key: string,
legacyKeys: string[] = []
): Promise<string | null> {
// First try main key
let value = await SecureStore.getItemAsync(key);
// If not found, try legacy keys
if (!value) {
for (const legacyKey of legacyKeys) {
console.log(`[Auth] Main key not found, trying legacy key: ${legacyKey}`);
value = await SecureStore.getItemAsync(legacyKey);
if (value) {
console.log(`[Auth] Found credential in legacy location: ${legacyKey}`);
break;
}
}
}
return value;
}
/**
* Resets the migration flag for testing purposes
* Only available in development builds
*/
export async function resetMigration(): Promise<void> {
if (__DEV__) {
console.log('[Auth] Resetting migration flag for testing');
await SecureStore.deleteItemAsync(MIGRATION_VERSION_KEY);
}
}

View File

@ -13,13 +13,15 @@ import * as SecureStore from 'expo-secure-store';
import { RelayService } from '@/lib/db/services/RelayService';
import { AuthService } from '@/lib/auth/AuthService';
import { SECURE_STORE_KEYS } from '@/lib/auth/constants';
// Feature flags for authentication systems
export const FLAGS = {
useReactQueryAuth: true, // When true, use React Query auth; when false, use legacy auth
useReactQueryAuth: false, // When true, use React Query auth; when false, use legacy auth
};
// Constants for SecureStore
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
const PRIVATE_KEY_STORAGE_KEY = SECURE_STORE_KEYS.PRIVATE_KEY;
// Default relays
const DEFAULT_RELAYS = [