diff --git a/docs/technical/index.md b/docs/technical/index.md index ed97d57..c6ce400 100644 --- a/docs/technical/index.md +++ b/docs/technical/index.md @@ -8,16 +8,19 @@ This section contains detailed technical documentation about specific technologi - [Caching](./caching/index.md) - Caching strategies and implementations - [Styling](./styling/index.md) - Styling approach and patterns - [Nostr](./nostr/index.md) - Nostr protocol implementation details +- [React Query](./react-query-integration.md) - Solution for authentication state, hook ordering, and data synchronization ## Key Technical Documents - [NDK Comprehensive Guide](./ndk/comprehensive_guide.md) - Complete guide to NDK implementation - [Cache Implementation](./caching/cache_implementation.md) - Detailed caching architecture - [Nostr Exercise NIP](./nostr/exercise_nip.md) - Nostr specification for exercise data +- [React Query Integration Plan](./react-query-integration.md) - Comprehensive plan for React Query implementation +- [React Query Executive Summary](./react-query-integration-summary.md) - High-level overview of React Query migration ## Related Documentation - [Architecture Documentation](../architecture/index.md) - System-wide architectural documentation - [Feature Documentation](../features/index.md) - Feature-specific implementations -**Last Updated:** 2025-03-25 +**Last Updated:** 2025-04-03 diff --git a/docs/technical/react-query-integration-summary.md b/docs/technical/react-query-integration-summary.md new file mode 100644 index 0000000..086b2b0 --- /dev/null +++ b/docs/technical/react-query-integration-summary.md @@ -0,0 +1,72 @@ +# React Query Integration - Executive Summary + +**Last Updated:** April 3, 2025 + +## Overview + +This document provides a high-level summary of our plan to integrate React Query (TanStack Query) into the POWR app to address critical issues with authentication state transitions, hook ordering problems, and to enhance our offline synchronization capabilities. + +## Current Challenges + +1. **Authentication State Transition Issues** + - UI freezes during auth transitions, especially with Amber signer + - Crashes caused by React hook ordering violations when auth state changes + - Inconsistent component behavior during sign-in/sign-out operations + +2. **Data Synchronization Challenges** + - Ad-hoc caching logic across components + - Inconsistent offline handling + - Difficult conflict resolution + +3. **Code Maintenance Problems** + - Duplicate loading/error state handling + - Complex conditional rendering based on auth state + - Disparate approaches to data fetching and caching + +## Proposed Solution + +Integrate React Query as a centralized data synchronization layer between our UI, SQLite persistence, and NDK/Nostr communication. + +### Key Benefits + +1. **Stabilized Authentication** + - Consistent hook ordering regardless of auth state + - Proper state transitions with React Query's state management + - Better error propagation for auth operations + +2. **Enhanced Data Management** + - Centralized caching with automatic invalidation + - Standardized loading/error states + - Consistent data access patterns + +3. **Improved Offline Support** + - Better background synchronization + - Enhanced conflict resolution strategies + - More resilient error recovery + +4. **Developer Experience** + - Simpler component implementation + - Better type safety throughout the app + - Easier testing and debugging + +## Implementation Approach + +We're planning a phased 8-week approach to minimize disruption: + +1. **Weeks 1-2**: Core infrastructure (Auth, Query Client) +2. **Weeks 3-4**: Social & Profile features (most critical areas) +3. **Weeks 5-6**: Workout & Exercise features +4. **Weeks 7-8**: System-wide integration & refinement + +## Impact on Architecture + +This integration preserves our existing architecture while enhancing its resilience: + +- SQLite remains our primary persistence layer +- NDK continues as our Nostr communication layer +- Zustand stores remain for UI and active workout state +- React Query serves as the synchronization layer between these components + +## Next Steps + +For details on implementation, please see the [full React Query integration plan](./react-query-integration.md). diff --git a/docs/technical/react-query-integration.md b/docs/technical/react-query-integration.md new file mode 100644 index 0000000..ff5ced9 --- /dev/null +++ b/docs/technical/react-query-integration.md @@ -0,0 +1,1698 @@ +# React Query Integration for POWR + +**Last Updated:** April 3, 2025 +**Status:** Implementation Plan +**Authors:** POWR Development Team + +## Table of Contents + +1. [Overview](#overview) +2. [Current Architecture Analysis](#current-architecture-analysis) +3. [React Query Integration Strategy](#react-query-integration-strategy) +4. [Authentication & State Management](#authentication--state-management) +5. [Data Fetching & Caching](#data-fetching--caching) +6. [Offline Support & Publication Queue](#offline-support--publication-queue) +7. [Error Handling](#error-handling) +8. [Conflict Resolution](#conflict-resolution) +9. [Implementation Roadmap](#implementation-roadmap) +10. [API Reference](#api-reference) +11. [Migration Guide](#migration-guide) + +## Overview + +This document outlines the plan to integrate React Query (TanStack Query) into the POWR app to resolve critical issues with authentication state transitions, hook ordering, and offline synchronization while preserving our local-first architecture with Nostr integration. + +### Key Goals + +- Fix React hook ordering issues causing crashes during authentication state changes +- Provide consistent data access patterns across the application +- Enhance offline support with better synchronization +- Improve error handling and recovery +- Maintain local-first architecture with SQLite and Nostr integration + +### Architecture Principles + +1. **SQLite Remains Primary Persistence Layer**: All user data will continue to be persisted in SQLite +2. **React Query as Sync/Cache Layer**: React Query manages data synchronization between UI, SQLite, and Nostr +3. **Zustand for UI State**: Active workout state and UI-specific state remain in Zustand stores +4. **NDK for Nostr Integration**: NDK remains the primary interface for Nostr communication + +## Current Architecture Analysis + +### Existing Data Flow + +``` +┌─ UI Components ─────────────────────────────────┐ +│ │ +│ React Components (screens, etc.) │ +│ │ +└─────────────────┬───────────────────────────────┘ + │ +┌─────────────────▼───────────────────────────────┐ +│ │ +│ Custom Hooks (useProfile, useSocialFeed, etc.) │ +│ │ +└───────┬───────────────────────┬─────────────────┘ + │ │ +┌───────▼───────────┐ ┌────────▼────────────────┐ +│ │ │ │ +│ SQLite Services │ │ NDK Integration │ +│ (Local Storage) │ │ (Nostr Communication) │ +│ │ │ │ +└───────────────────┘ └─────────────────────────┘ +``` + +### Current Authentication System + +The current authentication system is implemented across several components: + +- `lib/auth/AuthService.ts`: Core authentication service +- `lib/auth/AuthStateManager.ts`: State machine for auth transitions +- `lib/auth/SigningQueue.ts`: Queue for NDK event signing +- `lib/hooks/useNDK.ts`: Hook for accessing the NDK instance +- `lib/hooks/useNDKAuth.ts`: Hook for authentication with NDK +- `lib/signers/NDKAmberSigner.ts`: External signer integration for Android + +The system implements a state machine pattern: + +```typescript +type AuthState = + | { status: 'unauthenticated' } + | { status: 'authenticating', method: 'private_key' | 'amber' | 'ephemeral' } + | { status: 'authenticated', user: NDKUser, method: 'private_key' | 'amber' | 'ephemeral' } + | { status: 'signing', operationCount: number, operations: SigningOperation[] } + | { status: 'error', error: Error }; +``` + +However, hook ordering issues occur when components conditionally use hooks based on authentication state. + +### Current Caching System + +The current caching system includes: + +- `SocialFeedCache`: SQLite-based caching for social feed events +- `ProfileImageCache`: Caching for user profile images +- `ContactCacheService`: Caching for user contact lists +- `EventCache`: General-purpose cache for Nostr events +- `PublicationQueueService`: Queue for pending Nostr publications + +### NDK Integration + +NDK is integrated through: + +- `lib/stores/ndk.ts`: NDK store implementation +- `lib/hooks/useNDK.ts`: Hook for accessing NDK +- `lib/initNDK.ts`: NDK initialization +- Various hooks for specific NDK operations + +## React Query Integration Strategy + +### New Architecture with React Query + +``` +┌─ UI Components ─────────────────────────────────┐ +│ │ +│ React Components (screens, etc.) │ +│ │ +└─────────────────┬───────────────────────────────┘ + │ + ▼ +┌─ Data Access Layer ───────────────────────────┐ +│ │ +│ React Query Hooks │ +│ (useProfile, useWorkouts, useTemplates, etc.)│ +│ │ +└───────┬───────────────────────┬───────────────┘ + │ │ + ▼ ▼ +┌───────────────────┐ ┌────────────────────────┐ +│ SQLite Services │ │ NDK Integration │ +│ (local storage) │ │ (network/Nostr) │ +└───────────────────┘ └────────────────────────┘ +``` + +### Replacement Strategy + +Our strategy is to use React Query to replace portions of our custom hooks that handle data fetching, caching, and synchronization, while preserving our SQLite services and NDK integration. + +#### What React Query Will Replace + +- Custom caching logic in hooks +- Manual state management for loading/error states +- Ad-hoc fetch retry and synchronization logic +- Authentication state propagation + +#### What Will Remain Unchanged + +- SQLite database schema and services +- NDK event handling and Nostr communication +- Zustand stores for active workout state and UI-specific state +- Core business logic + +### Key Benefits + +1. **Consistent Hook Ordering**: React Query preserves hook call order across renders +2. **Automatic Background Refetching**: Built-in mechanisms for data freshness +3. **Standardized Loading/Error States**: Consistent data loading patterns +4. **Optimistic Updates**: Built-in support for optimistic UI updates +5. **Deduplication of Requests**: Prevents redundant network/database calls + +## Authentication & State Management + +### React Query Auth Provider + +We'll implement a centralized authentication system using React Query: + +```tsx +// lib/auth/ReactQueryAuthProvider.tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createContext, useContext, useState, ReactNode } from 'react'; +import { AuthService } from './AuthService'; + +// Create a new Query Client +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, // 5 minutes + cacheTime: 30 * 60 * 1000, // 30 minutes + retry: 3, + refetchOnWindowFocus: true, + refetchOnMount: true, + }, + }, +}); + +// Create a context for global access to auth methods +const AuthContext = createContext<{ + authService: AuthService | null; +}>({ authService: null }); + +// Provider component +export function ReactQueryAuthProvider({ children }: { children: ReactNode }) { + const [authService] = useState(() => new AuthService()); + + return ( + + + {children} + + + ); +} + +// Hook for accessing auth service +export function useAuthService() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuthService must be used within a ReactQueryAuthProvider'); + } + return context.authService; +} +``` + +### Auth Query Hook + +```typescript +// lib/hooks/useAuthQuery.ts +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useAuthService } from '../auth/ReactQueryAuthProvider'; +import * as SecureStore from 'expo-secure-store'; + +// Auth state query keys +export const AUTH_KEYS = { + all: ['auth'] as const, + current: () => [...AUTH_KEYS.all, 'current'] as const, + profile: (pubkey: string) => [...AUTH_KEYS.all, 'profile', pubkey] as const, +}; + +// Hook for managing auth state +export function useAuthQuery() { + const authService = useAuthService(); + const queryClient = useQueryClient(); + + // Query for current auth state + const authQuery = useQuery({ + queryKey: AUTH_KEYS.current(), + queryFn: async () => { + // Check for stored credentials + const pubkey = await SecureStore.getItemAsync('nostr_pubkey'); + + // If no credentials, return unauthenticated state + if (!pubkey) { + return { status: 'unauthenticated' }; + } + + // Initialize auth from stored credentials + try { + // For direct key login + return await authService.getCurrentAuthState(); + } catch (error) { + // Handle initialization error + console.error('Auth initialization error:', error); + + // Clear any invalid credentials + await SecureStore.deleteItemAsync('nostr_pubkey'); + + return { + status: 'error', + error: error instanceof Error ? error : new Error(String(error)) + }; + } + }, + // Auth doesn't automatically become stale + staleTime: Infinity, + }); + + // Login mutation + const loginMutation = useMutation({ + mutationFn: async (params: { + method: 'private_key' | 'amber' | 'ephemeral'; + privateKey?: string; + }) => { + // Set auth state to authenticating first + queryClient.setQueryData(AUTH_KEYS.current(), { + status: 'authenticating', + method: params.method + }); + + // Perform login based on method + try { + switch (params.method) { + case 'private_key': + if (!params.privateKey) throw new Error('Private key required'); + return await authService.loginWithPrivateKey(params.privateKey); + case 'amber': + return await authService.loginWithAmber(); + case 'ephemeral': + return await authService.createEphemeralKey(); + default: + throw new Error(`Unsupported auth method: ${params.method}`); + } + } catch (error) { + throw error; + } + }, + onSuccess: (user) => { + // Update auth state on success + queryClient.setQueryData(AUTH_KEYS.current(), { + status: 'authenticated', + user, + method: user.authMethod + }); + + // Invalidate auth-dependent queries + queryClient.invalidateQueries({ queryKey: ['profile'] }); + queryClient.invalidateQueries({ queryKey: ['contacts'] }); + queryClient.invalidateQueries({ queryKey: ['relays'] }); + }, + onError: (error) => { + // Set error state + queryClient.setQueryData(AUTH_KEYS.current(), { + status: 'error', + error + }); + } + }); + + // Logout mutation + const logoutMutation = useMutation({ + mutationFn: async () => { + // Perform logout + await authService.logout(); + }, + onSuccess: () => { + // Reset auth state + queryClient.setQueryData(AUTH_KEYS.current(), { + status: 'unauthenticated' + }); + + // Clear user-dependent queries + queryClient.removeQueries({ queryKey: ['contacts'] }); + queryClient.removeQueries({ queryKey: ['feed'] }); + queryClient.removeQueries({ queryKey: ['profile'] }); + } + }); + + return { + auth: authQuery.data, + isLoading: authQuery.isLoading, + isError: authQuery.isError, + error: authQuery.error, + login: loginMutation.mutate, + logout: logoutMutation.mutate, + isAuthenticating: loginMutation.isPending, + isLoggingOut: logoutMutation.isPending, + }; +} +``` + +### Amber Signer Integration + +```typescript +// lib/hooks/useAmberSignerWithQuery.ts +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useAuthQuery, AUTH_KEYS } from './useAuthQuery'; +import { NDKEvent } from '@nostr-dev-kit/ndk'; + +// Hook for Amber signing operations +export function useAmberSigner() { + const { auth } = useAuthQuery(); + const queryClient = useQueryClient(); + + // Mutation for signing events + const signEventMutation = useMutation({ + mutationFn: async (event: NDKEvent) => { + if (auth?.status !== 'authenticated') { + throw new Error('Not authenticated'); + } + + // Update auth state to show signing in progress + queryClient.setQueryData(AUTH_KEYS.current(), { + ...auth, + status: 'signing', + operationCount: (auth.operationCount || 0) + 1, + operations: [...(auth.operations || []), { + type: 'sign', + event, + timestamp: Date.now() + }] + }); + + try { + // Use AmberSignerModule to sign the event + const signedEvent = await AmberSignerModule.signEvent( + JSON.stringify(event.rawEvent()), + event.pubkey + ); + + // Restore auth state after signing + queryClient.setQueryData(AUTH_KEYS.current(), auth); + + return signedEvent; + } catch (error) { + // Update auth state with error + queryClient.setQueryData(AUTH_KEYS.current(), { + ...auth, + status: 'authenticated', + error + }); + + throw error; + } + } + }); + + return { + signEvent: signEventMutation.mutate, + isSigningEvent: signEventMutation.isPending, + signingError: signEventMutation.error + }; +} +``` + +### Authentication Component Updates + +Components will need to be updated to use the new auth hooks consistently: + +```tsx +// Example Login Screen +function LoginScreen() { + const { login, isAuthenticating, error } = useAuthQuery(); + const [privateKey, setPrivateKey] = useState(''); + + // Handle login + const handleLogin = () => { + login({ method: 'private_key', privateKey }); + }; + + // Handle Amber login + const handleAmberLogin = () => { + login({ method: 'amber' }); + }; + + return ( + + {isAuthenticating ? ( + + ) : ( + <> + +