1
0
mirror of https://github.com/DocNR/POWR.git synced 2025-05-11 00:35:54 +00:00
POWR/docs/features/profile/authentication_patterns.md
DocNR 969163313a fix(auth): Improve authentication state handling and avatar display
* Add style prop to UserAvatar component for better customization
* Refactor UserAvatar to use getAvatarSeed utility for consistent avatar generation
* Fix React hook ordering issues in profile/overview.tsx to prevent crashes during auth state changes
* Add proper state initialization and cleanup during authentication transitions
* Ensure consistent fallback avatar display for unauthenticated users

These changes improve stability during login/logout operations and provide better visual continuity with Robohash avatars when profile images aren't available.
2025-04-02 21:11:25 -04:00

6.6 KiB

Authentication Patterns

Last Updated: 2025-04-02
Status: Implemented
Related To: Profile Tab Overview, Nostr Integration

Introduction

This document outlines the authentication patterns used throughout the Profile tab and other parts of the application that integrate with Nostr. These patterns ensure consistent handling of authenticated and unauthenticated states while maintaining React hook ordering consistency.

Key Challenges

When implementing authentication in React components, especially with React Navigation and conditional rendering, several key challenges needed to be addressed:

  1. Hook Ordering: React requires hooks to be called in the same order on every render
  2. Early Returns: Authentication checks often lead to early returns, which can cause inconsistent hook calls
  3. Conditional Hook Calling: Different UI for authenticated vs. unauthenticated states can lead to conditional hook usage

Solution Pattern

The application implements a consistent pattern for handling authentication across components:

1. Define All Hooks Unconditionally

All hooks must be defined at the top level of the component, before any conditional returns:

// Component setup and imports...

export default function ProfileComponent() {
  // Define all hooks at the top level, regardless of authentication state
  const { isAuthenticated, currentUser } = useNDKCurrentUser();
  const theme = useTheme() as CustomTheme;
  const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
  
  // Even hooks that are only needed in authenticated state should be called
  // with safeguards to prevent errors
  const analytics = useAnalytics();
  const stats = isAuthenticated ? useProfileStats({ pubkey: currentUser?.pubkey || '' }) : { followersCount: 0, followingCount: 0 };
  
  // Define all callback functions before any conditional returns
  const handleSomeAction = useCallback(() => {
    // Implementation...
  }, [dependencies]);
  
  // Only after all hooks are defined, use conditional rendering
  if (!isAuthenticated) {
    return <NostrProfileLogin message="Appropriate message" />;
  }
  
  // Main component rendering logic for authenticated state...
}

2. Use Memoized Sub-Components for Complex Conditional Logic

For more complex components that have multiple conditional UI elements:

// Define separate components for different rendering scenarios
const AuthenticatedView = React.memo(() => {
  // Can safely use hooks here too, as this component itself
  // is unconditionally rendered by the parent
  const someHook = useSomeHook();
  
  return (
    <View>
      {/* Authenticated UI */}
    </View>
  );
});

// Main component with consistent hook ordering
export default function Component() {
  const { isAuthenticated } = useNDKCurrentUser();
  // Other hooks...
  
  // Then use conditional rendering
  return (
    <View>
      {isAuthenticated ? <AuthenticatedView /> : <NostrProfileLogin />}
    </View>
  );
}

3. Safe Hook Usage with Fallbacks

When a hook is only useful in authenticated state but must be called regardless:

// For hooks that need authentication but must be called every time
const { data, loading } = isAuthenticated 
  ? useSomeDataHook({ userId: currentUser?.pubkey }) 
  : { data: null, loading: false };

Real-World Example

In the Profile Overview screen, follower statistics are handled safely regardless of authentication state:

// Profile data including fallbacks
const profileImageUrl = currentUser?.profile?.image || 
                       currentUser?.profile?.picture || 
                       (currentUser?.profile as any)?.avatar;
                       
// Follower stats component - always call useProfileStats hook 
// even if isAuthenticated is false (passing empty pubkey)
// This ensures consistent hook ordering regardless of authentication state
const { followersCount, followingCount, isLoading: statsLoading } = useProfileStats({ 
  pubkey: pubkey || '', 
  refreshInterval: 60000 * 15 // refresh every 15 minutes
});

// Use a separate component to avoid conditionally rendered hooks
const ProfileFollowerStats = React.memo(() => {
  return (
    <View className="flex-row mb-2">
      <TouchableOpacity className="mr-4">
        <Text>
          <Text className="font-bold">{statsLoading ? '...' : followingCount.toLocaleString()}</Text>
          <Text className="text-muted-foreground"> following</Text>
        </Text>
      </TouchableOpacity>
      
      <TouchableOpacity>
        <Text>
          <Text className="font-bold">{statsLoading ? '...' : followersCount.toLocaleString()}</Text>
          <Text className="text-muted-foreground"> followers</Text>
        </Text>
      </TouchableOpacity>
    </View>
  );
});

NostrProfileLogin Component

All tabs use the NostrProfileLogin component for unauthenticated states:

// Show different UI when not authenticated
if (!isAuthenticated) {
  return <NostrProfileLogin message="Login-specific message for this screen" />;
}

The NostrProfileLogin component provides a consistent login interface with a customizable message parameter to contextualize the login prompt based on the specific tab.

Authentication State Management

The isAuthenticated state comes from the useNDKCurrentUser hook:

const { currentUser, isAuthenticated } = useNDKCurrentUser();

This hook, provided by the NDK integration, handles checking if a user is authenticated with Nostr, and provides access to the user's profile information and public key.

Logout Flow

Logout functionality is implemented in the Settings tab using the useNDKAuth hook:

const { logout } = useNDKAuth();

// Logout button
<Button 
  variant="destructive"
  onPress={logout}
  className="w-full"
>
  <Text className="text-white">Logout</Text>
</Button>

The logout flow properly clears credentials and updates the isAuthenticated state, which propagates to all components using the NDK context.

Benefits of This Approach

This authentication pattern provides several benefits:

  1. Consistency: Authentication is handled consistently across all components
  2. Reliability: Avoids React hook ordering errors
  3. Maintainability: Clear pattern for developers to follow
  4. User Experience: Consistent login interfaces across the application
  5. Performance: Prevents unnecessary re-renders and hook calls