React Native Integration

React Native Integration

This guide covers how to integrate Skycloak authentication into React Native applications using react-native-app-auth and other modern authentication patterns.

Prerequisites

  • React Native 0.70+
  • Node.js 16+
  • Skycloak cluster with configured realm and mobile client
  • iOS 12.0+ / Android 5.0+ (API level 21+)
  • Basic understanding of React Native development

Quick Start

1. Install Dependencies

npm install react-native-app-auth react-native-keychain react-native-inappbrowser-reborn
npm install jwt-decode @react-native-async-storage/async-storage
npm install react-native-vector-icons @react-navigation/native @react-navigation/stack

# For iOS
cd ios && pod install

2. Platform Configuration

iOS Configuration

Update ios/YourApp/Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>com.yourcompany.app</string>
        </array>
    </dict>
</array>

Update ios/YourApp/AppDelegate.mm:

#import <React/RCTLinkingManager.h>

// Add this method
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options
{
  return [RCTLinkingManager application:app openURL:url options:options];
}

Android Configuration

Update android/app/build.gradle:

android {
    defaultConfig {
        manifestPlaceholders = [
            appAuthRedirectScheme: 'com.yourcompany.app'
        ]
    }
}

Update android/app/src/main/AndroidManifest.xml:

<!-- Add inside <application> tag -->
<activity
    android:name="net.openid.appauth.RedirectUriReceiverActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="com.yourcompany.app" />
    </intent-filter>
</activity>

3. Create Auth Configuration

// src/config/auth.config.ts
export const AuthConfig = {
  issuer: 'https://your-cluster-id.app.skycloak.io/realms/your-realm',
  clientId: 'your-react-native-app',
  redirectUrl: 'com.yourcompany.app://oauth/callback',
  postLogoutRedirectUrl: 'com.yourcompany.app://oauth/logout',
  scopes: ['openid', 'profile', 'email', 'offline_access'],
  additionalParameters: {},
  customHeaders: {},
  
  // Optional: For Android 11+ to handle Chrome Custom Tabs
  warmAndPrefetchChrome: true,
  
  // Optional: Use ephemeral session (doesn't save cookies)
  useNonce: true,
  usePKCE: true,
};

4. Create Auth Service

// src/services/AuthService.ts
import { authorize, refresh, revoke, AuthConfiguration } from 'react-native-app-auth';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Keychain from 'react-native-keychain';
import { decode as jwtDecode } from 'jwt-decode';
import { AuthConfig } from '../config/auth.config';

interface AuthState {
  hasLoggedIn: boolean;
  accessToken: string | null;
  refreshToken: string | null;
  idToken: string | null;
  accessTokenExpirationDate: string | null;
  userInfo: UserInfo | null;
}

interface UserInfo {
  id: string;
  username: string;
  email: string;
  name: string;
  roles: string[];
  groups: string[];
}

interface JWTToken {
  sub: string;
  preferred_username: string;
  email: string;
  name: string;
  realm_access?: {
    roles: string[];
  };
  resource_access?: {
    [key: string]: {
      roles: string[];
    };
  };
  groups?: string[];
  exp: number;
}

class AuthService {
  private static instance: AuthService;
  private authState: AuthState = {
    hasLoggedIn: false,
    accessToken: null,
    refreshToken: null,
    idToken: null,
    accessTokenExpirationDate: null,
    userInfo: null,
  };

  private constructor() {
    this.loadAuthState();
  }

  static getInstance(): AuthService {
    if (!AuthService.instance) {
      AuthService.instance = new AuthService();
    }
    return AuthService.instance;
  }

  // Initialize auth state from storage
  private async loadAuthState(): Promise<void> {
    try {
      const credentials = await Keychain.getInternetCredentials('skycloak_auth');
      if (credentials) {
        const tokens = JSON.parse(credentials.password);
        this.authState = {
          hasLoggedIn: true,
          accessToken: tokens.accessToken,
          refreshToken: tokens.refreshToken,
          idToken: tokens.idToken,
          accessTokenExpirationDate: tokens.accessTokenExpirationDate,
          userInfo: this.extractUserInfo(tokens.idToken),
        };
      }
    } catch (error) {
      console.error('Failed to load auth state:', error);
    }
  }

  // Save auth state to secure storage
  private async saveAuthState(): Promise<void> {
    try {
      if (this.authState.hasLoggedIn && this.authState.accessToken) {
        await Keychain.setInternetCredentials(
          'skycloak_auth',
          'tokens',
          JSON.stringify({
            accessToken: this.authState.accessToken,
            refreshToken: this.authState.refreshToken,
            idToken: this.authState.idToken,
            accessTokenExpirationDate: this.authState.accessTokenExpirationDate,
          })
        );
      } else {
        await Keychain.resetInternetCredentials('skycloak_auth');
      }
    } catch (error) {
      console.error('Failed to save auth state:', error);
    }
  }

  // Extract user info from ID token
  private extractUserInfo(idToken: string | null): UserInfo | null {
    if (!idToken) return null;

    try {
      const decoded = jwtDecode<JWTToken>(idToken);
      
      // Extract roles
      const realmRoles = decoded.realm_access?.roles || [];
      const clientRoles = decoded.resource_access?.[AuthConfig.clientId]?.roles || [];
      const allRoles = [...realmRoles, ...clientRoles];

      return {
        id: decoded.sub,
        username: decoded.preferred_username,
        email: decoded.email,
        name: decoded.name,
        roles: allRoles,
        groups: decoded.groups || [],
      };
    } catch (error) {
      console.error('Failed to decode ID token:', error);
      return null;
    }
  }

  // Login
  async login(): Promise<void> {
    try {
      const result = await authorize(AuthConfig);
      
      this.authState = {
        hasLoggedIn: true,
        accessToken: result.accessToken,
        refreshToken: result.refreshToken,
        idToken: result.idToken,
        accessTokenExpirationDate: result.accessTokenExpirationDate,
        userInfo: this.extractUserInfo(result.idToken),
      };

      await this.saveAuthState();
    } catch (error) {
      console.error('Authentication failed:', error);
      throw error;
    }
  }

  // Refresh token
  async refreshToken(): Promise<void> {
    if (!this.authState.refreshToken) {
      throw new Error('No refresh token available');
    }

    try {
      const result = await refresh(AuthConfig, {
        refreshToken: this.authState.refreshToken,
      });

      this.authState = {
        ...this.authState,
        accessToken: result.accessToken,
        refreshToken: result.refreshToken || this.authState.refreshToken,
        idToken: result.idToken,
        accessTokenExpirationDate: result.accessTokenExpirationDate,
        userInfo: this.extractUserInfo(result.idToken),
      };

      await this.saveAuthState();
    } catch (error) {
      console.error('Token refresh failed:', error);
      // If refresh fails, clear auth state
      await this.logout();
      throw error;
    }
  }

  // Logout
  async logout(): Promise<void> {
    try {
      if (this.authState.idToken) {
        await revoke(AuthConfig, {
          tokenToRevoke: this.authState.idToken,
          includeBasicAuth: false,
          sendClientId: true,
        });
      }
    } catch (error) {
      console.error('Revoke token failed:', error);
    }

    // Clear auth state
    this.authState = {
      hasLoggedIn: false,
      accessToken: null,
      refreshToken: null,
      idToken: null,
      accessTokenExpirationDate: null,
      userInfo: null,
    };

    await Keychain.resetInternetCredentials('skycloak_auth');
  }

  // Get access token (with auto-refresh)
  async getAccessToken(): Promise<string | null> {
    if (!this.authState.accessToken) {
      return null;
    }

    // Check if token is expired
    if (this.isTokenExpired()) {
      try {
        await this.refreshToken();
      } catch (error) {
        return null;
      }
    }

    return this.authState.accessToken;
  }

  // Check if token is expired
  private isTokenExpired(): boolean {
    if (!this.authState.accessTokenExpirationDate) {
      return true;
    }

    const expirationDate = new Date(this.authState.accessTokenExpirationDate);
    const now = new Date();
    // Refresh if less than 5 minutes remaining
    const bufferTime = 5 * 60 * 1000;
    
    return expirationDate.getTime() - now.getTime() < bufferTime;
  }

  // Get current auth state
  getAuthState(): AuthState {
    return { ...this.authState };
  }

  // Check if user is authenticated
  isAuthenticated(): boolean {
    return this.authState.hasLoggedIn && !!this.authState.accessToken;
  }

  // Get user info
  getUserInfo(): UserInfo | null {
    return this.authState.userInfo;
  }

  // Check if user has a specific role
  hasRole(role: string): boolean {
    return this.authState.userInfo?.roles.includes(role) || false;
  }

  // Check if user has any of the specified roles
  hasAnyRole(...roles: string[]): boolean {
    const userRoles = this.authState.userInfo?.roles || [];
    return roles.some(role => userRoles.includes(role));
  }

  // Check if user has all specified roles
  hasAllRoles(...roles: string[]): boolean {
    const userRoles = this.authState.userInfo?.roles || [];
    return roles.every(role => userRoles.includes(role));
  }

  // Check if user is in a specific group
  inGroup(group: string): boolean {
    return this.authState.userInfo?.groups.includes(group) || false;
  }
}

export default AuthService.getInstance();

5. Create Auth Context

// src/contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import AuthService from '../services/AuthService';

interface AuthContextType {
  isAuthenticated: boolean;
  isLoading: boolean;
  userInfo: UserInfo | null;
  login: () => Promise<void>;
  logout: () => Promise<void>;
  refreshToken: () => Promise<void>;
  hasRole: (role: string) => boolean;
  hasAnyRole: (...roles: string[]) => boolean;
  hasAllRoles: (...roles: string[]) => boolean;
  inGroup: (group: string) => boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

interface AuthProviderProps {
  children: ReactNode;
}

export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [userInfo, setUserInfo] = useState<UserInfo | null>(null);

  useEffect(() => {
    checkAuthState();
  }, []);

  const checkAuthState = async () => {
    try {
      const authState = AuthService.getAuthState();
      setIsAuthenticated(authState.hasLoggedIn);
      setUserInfo(authState.userInfo);
      
      // Try to refresh token if authenticated
      if (authState.hasLoggedIn) {
        try {
          await AuthService.refreshToken();
          const newState = AuthService.getAuthState();
          setUserInfo(newState.userInfo);
        } catch (error) {
          // Refresh failed, user needs to login again
          setIsAuthenticated(false);
          setUserInfo(null);
        }
      }
    } catch (error) {
      console.error('Failed to check auth state:', error);
    } finally {
      setIsLoading(false);
    }
  };

  const login = async () => {
    setIsLoading(true);
    try {
      await AuthService.login();
      const authState = AuthService.getAuthState();
      setIsAuthenticated(true);
      setUserInfo(authState.userInfo);
    } finally {
      setIsLoading(false);
    }
  };

  const logout = async () => {
    setIsLoading(true);
    try {
      await AuthService.logout();
      setIsAuthenticated(false);
      setUserInfo(null);
    } finally {
      setIsLoading(false);
    }
  };

  const refreshToken = async () => {
    await AuthService.refreshToken();
    const authState = AuthService.getAuthState();
    setUserInfo(authState.userInfo);
  };

  const value: AuthContextType = {
    isAuthenticated,
    isLoading,
    userInfo,
    login,
    logout,
    refreshToken,
    hasRole: (role) => AuthService.hasRole(role),
    hasAnyRole: (...roles) => AuthService.hasAnyRole(...roles),
    hasAllRoles: (...roles) => AuthService.hasAllRoles(...roles),
    inGroup: (group) => AuthService.inGroup(group),
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

UI Implementation

Login Screen

// src/screens/LoginScreen.tsx
import React from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  ActivityIndicator,
  Image,
  SafeAreaView,
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { useAuth } from '../contexts/AuthContext';

const LoginScreen: React.FC = () => {
  const { login, isLoading } = useAuth();
  const [error, setError] = React.useState<string | null>(null);

  const handleLogin = async () => {
    try {
      setError(null);
      await login();
    } catch (err) {
      setError('Authentication failed. Please try again.');
      console.error('Login error:', err);
    }
  };

  return (
    <LinearGradient
      colors={['#4c669f', '#3b5998', '#192f6a']}
      style={styles.container}
    >
      <SafeAreaView style={styles.safeArea}>
        <View style={styles.content}>
          <View style={styles.logoContainer}>
            <Icon name="lock" size={80} color="#fff" />
            <Text style={styles.title}>MyApp</Text>
            <Text style={styles.subtitle}>Secure Authentication</Text>
          </View>

          {error && (
            <View style={styles.errorContainer}>
              <Text style={styles.errorText}>{error}</Text>
            </View>
          )}

          <TouchableOpacity
            style={[styles.loginButton, isLoading && styles.disabledButton]}
            onPress={handleLogin}
            disabled={isLoading}
          >
            {isLoading ? (
              <ActivityIndicator color="#fff" />
            ) : (
              <>
                <Icon name="login" size={24} color="#fff" style={styles.buttonIcon} />
                <Text style={styles.loginButtonText}>Sign In with Skycloak</Text>
              </>
            )}
          </TouchableOpacity>

          <TouchableOpacity
            style={styles.registerButton}
            onPress={handleLogin}
            disabled={isLoading}
          >
            <Text style={styles.registerButtonText}>Create Account</Text>
          </TouchableOpacity>
        </View>
      </SafeAreaView>
    </LinearGradient>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  safeArea: {
    flex: 1,
  },
  content: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 40,
  },
  logoContainer: {
    alignItems: 'center',
    marginBottom: 60,
  },
  title: {
    fontSize: 36,
    fontWeight: 'bold',
    color: '#fff',
    marginTop: 20,
  },
  subtitle: {
    fontSize: 18,
    color: '#fff',
    opacity: 0.8,
    marginTop: 10,
  },
  errorContainer: {
    backgroundColor: 'rgba(255, 0, 0, 0.1)',
    padding: 15,
    borderRadius: 8,
    marginBottom: 20,
    borderWidth: 1,
    borderColor: 'rgba(255, 0, 0, 0.3)',
  },
  errorText: {
    color: '#fff',
    textAlign: 'center',
  },
  loginButton: {
    backgroundColor: 'rgba(255, 255, 255, 0.2)',
    paddingVertical: 15,
    paddingHorizontal: 30,
    borderRadius: 25,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: 15,
    borderWidth: 1,
    borderColor: 'rgba(255, 255, 255, 0.3)',
  },
  disabledButton: {
    opacity: 0.7,
  },
  buttonIcon: {
    marginRight: 10,
  },
  loginButtonText: {
    color: '#fff',
    fontSize: 18,
    fontWeight: '600',
  },
  registerButton: {
    paddingVertical: 15,
  },
  registerButtonText: {
    color: '#fff',
    fontSize: 16,
    textAlign: 'center',
    textDecorationLine: 'underline',
  },
});

export default LoginScreen;

Main App Navigation

// src/navigation/AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { useAuth } from '../contexts/AuthContext';
import LoginScreen from '../screens/LoginScreen';
import HomeScreen from '../screens/HomeScreen';
import ProfileScreen from '../screens/ProfileScreen';
import AdminScreen from '../screens/AdminScreen';
import LoadingScreen from '../screens/LoadingScreen';

const Stack = createStackNavigator();
const Tab = createBottomTabNavigator();

const MainTabs: React.FC = () => {
  const { hasRole } = useAuth();

  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused, color, size }) => {
          let iconName: string;

          if (route.name === 'Home') {
            iconName = 'home';
          } else if (route.name === 'Profile') {
            iconName = 'person';
          } else if (route.name === 'Admin') {
            iconName = 'settings';
          } else {
            iconName = 'circle';
          }

          return <Icon name={iconName} size={size} color={color} />;
        },
        tabBarActiveTintColor: '#3b5998',
        tabBarInactiveTintColor: 'gray',
      })}
    >
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
      {hasRole('admin') && (
        <Tab.Screen name="Admin" component={AdminScreen} />
      )}
    </Tab.Navigator>
  );
};

const AppNavigator: React.FC = () => {
  const { isAuthenticated, isLoading } = useAuth();

  if (isLoading) {
    return <LoadingScreen />;
  }

  return (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        {isAuthenticated ? (
          <Stack.Screen name="Main" component={MainTabs} />
        ) : (
          <Stack.Screen name="Login" component={LoginScreen} />
        )}
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default AppNavigator;

Protected Components

// src/components/ProtectedView.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useAuth } from '../contexts/AuthContext';

interface ProtectedViewProps {
  roles?: string[];
  requireAll?: boolean;
  fallback?: React.ReactNode;
  children: React.ReactNode;
}

const ProtectedView: React.FC<ProtectedViewProps> = ({
  roles = [],
  requireAll = false,
  fallback,
  children,
}) => {
  const { hasAnyRole, hasAllRoles } = useAuth();

  const hasAccess = roles.length === 0 || 
    (requireAll ? hasAllRoles(...roles) : hasAnyRole(...roles));

  if (!hasAccess) {
    return fallback ? (
      <>{fallback}</>
    ) : (
      <View style={styles.accessDenied}>
        <Text style={styles.accessDeniedText}>
          You don't have permission to view this content
        </Text>
      </View>
    );
  }

  return <>{children}</>;
};

const styles = StyleSheet.create({
  accessDenied: {
    padding: 20,
    alignItems: 'center',
  },
  accessDeniedText: {
    color: '#666',
    fontSize: 16,
  },
});

export default ProtectedView;

Home Screen

// src/screens/HomeScreen.tsx
import React, { useState, useEffect } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  TouchableOpacity,
  RefreshControl,
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { useAuth } from '../contexts/AuthContext';
import ProtectedView from '../components/ProtectedView';
import ApiService from '../services/ApiService';

const HomeScreen: React.FC = () => {
  const { userInfo, logout } = useAuth();
  const [refreshing, setRefreshing] = useState(false);
  const [dashboardData, setDashboardData] = useState<any>(null);

  useEffect(() => {
    loadDashboardData();
  }, []);

  const loadDashboardData = async () => {
    try {
      const data = await ApiService.getDashboardData();
      setDashboardData(data);
    } catch (error) {
      console.error('Failed to load dashboard data:', error);
    }
  };

  const onRefresh = async () => {
    setRefreshing(true);
    await loadDashboardData();
    setRefreshing(false);
  };

  const handleLogout = async () => {
    await logout();
  };

  return (
    <ScrollView
      style={styles.container}
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
      }
    >
      <View style={styles.header}>
        <Text style={styles.welcomeText}>
          Welcome, {userInfo?.name || 'User'}!
        </Text>
        <TouchableOpacity onPress={handleLogout}>
          <Icon name="logout" size={24} color="#666" />
        </TouchableOpacity>
      </View>

      <View style={styles.card}>
        <Text style={styles.cardTitle}>Your Dashboard</Text>
        <Text style={styles.cardDescription}>
          View your personalized content and metrics
        </Text>
      </View>

      <ProtectedView roles={['admin', 'manager']}>
        <View style={styles.card}>
          <Text style={styles.cardTitle}>Management Tools</Text>
          <Text style={styles.cardDescription}>
            Access administrative features
          </Text>
        </View>
      </ProtectedView>

      <ProtectedView roles={['editor', 'moderator']}>
        <View style={styles.card}>
          <Text style={styles.cardTitle}>Content Management</Text>
          <Text style={styles.cardDescription}>
            Create and manage content
          </Text>
        </View>
      </ProtectedView>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 20,
    backgroundColor: '#fff',
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
  },
  welcomeText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#333',
  },
  card: {
    backgroundColor: '#fff',
    padding: 20,
    marginHorizontal: 20,
    marginTop: 20,
    borderRadius: 10,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.1,
    shadowRadius: 3.84,
    elevation: 5,
  },
  cardTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 8,
  },
  cardDescription: {
    fontSize: 14,
    color: '#666',
  },
});

export default HomeScreen;

API Integration

API Service

// src/services/ApiService.ts
import AuthService from './AuthService';

interface RequestOptions extends RequestInit {
  authenticated?: boolean;
}

class ApiService {
  private baseURL: string;

  constructor() {
    this.baseURL = 'https://api.example.com';
  }

  private async request<T>(
    endpoint: string,
    options: RequestOptions = {}
  ): Promise<T> {
    const { authenticated = true, ...fetchOptions } = options;

    const url = `${this.baseURL}${endpoint}`;
    const headers: HeadersInit = {
      'Content-Type': 'application/json',
      ...fetchOptions.headers,
    };

    if (authenticated) {
      const token = await AuthService.getAccessToken();
      if (token) {
        headers.Authorization = `Bearer ${token}`;
      } else {
        throw new Error('No access token available');
      }
    }

    try {
      const response = await fetch(url, {
        ...fetchOptions,
        headers,
      });

      if (!response.ok) {
        if (response.status === 401) {
          // Token might be expired, try refreshing
          await AuthService.refreshToken();
          // Retry the request with new token
          const newToken = await AuthService.getAccessToken();
          if (newToken) {
            headers.Authorization = `Bearer ${newToken}`;
            const retryResponse = await fetch(url, {
              ...fetchOptions,
              headers,
            });
            
            if (!retryResponse.ok) {
              throw new Error(`API Error: ${retryResponse.statusText}`);
            }
            
            return retryResponse.json();
          }
        }
        
        throw new Error(`API Error: ${response.statusText}`);
      }

      return response.json();
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }

  // API Methods
  async getDashboardData() {
    return this.request<any>('/dashboard');
  }

  async getUserProfile() {
    return this.request<any>('/user/profile');
  }

  async updateUserProfile(data: any) {
    return this.request<any>('/user/profile', {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  async getUsers(page: number = 1) {
    return this.request<any>(`/admin/users?page=${page}`);
  }

  async deleteUser(userId: string) {
    return this.request<any>(`/admin/users/${userId}`, {
      method: 'DELETE',
    });
  }
}

export default new ApiService();

Hooks for API Calls

// src/hooks/useApi.ts
import { useState, useEffect, useCallback } from 'react';

interface UseApiOptions {
  immediate?: boolean;
}

interface UseApiResult<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  execute: (...args: any[]) => Promise<void>;
  reset: () => void;
}

export function useApi<T>(
  apiFunction: (...args: any[]) => Promise<T>,
  options: UseApiOptions = {}
): UseApiResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  const execute = useCallback(async (...args: any[]) => {
    setLoading(true);
    setError(null);

    try {
      const result = await apiFunction(...args);
      setData(result);
    } catch (err) {
      setError(err as Error);
    } finally {
      setLoading(false);
    }
  }, [apiFunction]);

  const reset = useCallback(() => {
    setData(null);
    setError(null);
    setLoading(false);
  }, []);

  useEffect(() => {
    if (options.immediate) {
      execute();
    }
  }, []);

  return { data, loading, error, execute, reset };
}

// Usage example
const ProfileScreen: React.FC = () => {
  const { data: profile, loading, error, execute: loadProfile } = useApi(
    ApiService.getUserProfile,
    { immediate: true }
  );

  // Component implementation
};

Testing

Unit Tests

// __tests__/AuthService.test.ts
import AuthService from '../src/services/AuthService';
import { authorize, refresh, revoke } from 'react-native-app-auth';
import * as Keychain from 'react-native-keychain';

jest.mock('react-native-app-auth');
jest.mock('react-native-keychain');

describe('AuthService', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('login should store tokens', async () => {
    const mockAuthResult = {
      accessToken: 'mock-access-token',
      refreshToken: 'mock-refresh-token',
      idToken: 'mock-id-token',
      accessTokenExpirationDate: '2024-12-31T23:59:59Z',
    };

    (authorize as jest.Mock).mockResolvedValue(mockAuthResult);
    (Keychain.setInternetCredentials as jest.Mock).mockResolvedValue(true);

    await AuthService.login();

    expect(authorize).toHaveBeenCalled();
    expect(Keychain.setInternetCredentials).toHaveBeenCalledWith(
      'skycloak_auth',
      'tokens',
      expect.any(String)
    );
  });

  test('logout should clear tokens', async () => {
    (revoke as jest.Mock).mockResolvedValue(undefined);
    (Keychain.resetInternetCredentials as jest.Mock).mockResolvedValue(true);

    await AuthService.logout();

    expect(Keychain.resetInternetCredentials).toHaveBeenCalledWith('skycloak_auth');
    expect(AuthService.isAuthenticated()).toBe(false);
  });

  test('hasRole should check user roles', () => {
    // Mock auth state with roles
    const mockUserInfo = {
      id: '123',
      username: 'testuser',
      email: '[email protected]',
      name: 'Test User',
      roles: ['user', 'admin'],
      groups: [],
    };

    // Use reflection to set private property (for testing only)
    (AuthService as any).authState.userInfo = mockUserInfo;

    expect(AuthService.hasRole('admin')).toBe(true);
    expect(AuthService.hasRole('superadmin')).toBe(false);
  });
});

Integration Tests

// __tests__/integration/AuthFlow.test.tsx
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import AppNavigator from '../src/navigation/AppNavigator';
import { AuthProvider } from '../src/contexts/AuthContext';
import AuthService from '../src/services/AuthService';

jest.mock('../src/services/AuthService');

describe('Authentication Flow', () => {
  test('shows login screen when not authenticated', () => {
    (AuthService.isAuthenticated as jest.Mock).mockReturnValue(false);

    const { getByText } = render(
      <AuthProvider>
        <NavigationContainer>
          <AppNavigator />
        </NavigationContainer>
      </AuthProvider>
    );

    expect(getByText('Sign In with Skycloak')).toBeTruthy();
  });

  test('shows home screen when authenticated', async () => {
    (AuthService.isAuthenticated as jest.Mock).mockReturnValue(true);
    (AuthService.getAuthState as jest.Mock).mockReturnValue({
      hasLoggedIn: true,
      userInfo: {
        name: 'Test User',
        roles: ['user'],
      },
    });

    const { getByText } = render(
      <AuthProvider>
        <NavigationContainer>
          <AppNavigator />
        </NavigationContainer>
      </AuthProvider>
    );

    await waitFor(() => {
      expect(getByText(/Welcome, Test User/)).toBeTruthy();
    });
  });
});

Production Considerations

Security Configuration

// src/utils/security.ts
import JailMonkey from 'jail-monkey';
import { NativeModules, Platform } from 'react-native';

export class SecurityManager {
  static isDeviceSecure(): boolean {
    // Check for jailbreak/root
    if (JailMonkey.isJailBroken()) {
      console.warn('Device is jailbroken/rooted');
      return false;
    }

    // Check for debugger
    if (JailMonkey.isDebuggedMode()) {
      console.warn('App is being debugged');
      return false;
    }

    // Check for external storage (Android)
    if (Platform.OS === 'android' && JailMonkey.isOnExternalStorage()) {
      console.warn('App is installed on external storage');
      return false;
    }

    return true;
  }

  static enableCertificatePinning() {
    // Implement certificate pinning
    // This typically requires native module implementation
    if (NativeModules.CertificatePinner) {
      NativeModules.CertificatePinner.pin([
        {
          hostname: 'your-cluster-id.app.skycloak.io',
          pin: 'sha256/YOUR_CERTIFICATE_PIN',
        },
        {
          hostname: 'api.example.com',
          pin: 'sha256/YOUR_API_CERTIFICATE_PIN',
        },
      ]);
    }
  }

  static obfuscateString(str: string): string {
    // Simple obfuscation for sensitive strings
    return Buffer.from(str).toString('base64');
  }

  static deobfuscateString(str: string): string {
    return Buffer.from(str, 'base64').toString('utf-8');
  }
}

Performance Optimization

// src/utils/performance.ts
import React, { memo, useCallback, useMemo } from 'react';
import { InteractionManager } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

// Memoized component wrapper
export function withMemo<P extends object>(
  Component: React.ComponentType<P>,
  propsAreEqual?: (prevProps: P, nextProps: P) => boolean
) {
  return memo(Component, propsAreEqual);
}

// Debounce hook
export function useDebounce<T extends (...args: any[]) => any>(
  callback: T,
  delay: number
): T {
  const timeoutRef = React.useRef<NodeJS.Timeout>();

  const debouncedCallback = useCallback(
    (...args: Parameters<T>) => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }

      timeoutRef.current = setTimeout(() => {
        callback(...args);
      }, delay);
    },
    [callback, delay]
  ) as T;

  React.useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);

  return debouncedCallback;
}

// Cache manager
export class CacheManager {
  private static cacheKeyPrefix = '@app_cache:';

  static async get<T>(key: string): Promise<T | null> {
    try {
      const cachedData = await AsyncStorage.getItem(
        `${this.cacheKeyPrefix}${key}`
      );
      
      if (cachedData) {
        const { data, expiry } = JSON.parse(cachedData);
        
        if (expiry && new Date().getTime() > expiry) {
          await this.remove(key);
          return null;
        }
        
        return data;
      }
      
      return null;
    } catch (error) {
      console.error('Cache get error:', error);
      return null;
    }
  }

  static async set<T>(
    key: string,
    data: T,
    expiryMinutes?: number
  ): Promise<void> {
    try {
      const cacheData = {
        data,
        expiry: expiryMinutes
          ? new Date().getTime() + expiryMinutes * 60 * 1000
          : null,
      };
      
      await AsyncStorage.setItem(
        `${this.cacheKeyPrefix}${key}`,
        JSON.stringify(cacheData)
      );
    } catch (error) {
      console.error('Cache set error:', error);
    }
  }

  static async remove(key: string): Promise<void> {
    try {
      await AsyncStorage.removeItem(`${this.cacheKeyPrefix}${key}`);
    } catch (error) {
      console.error('Cache remove error:', error);
    }
  }

  static async clear(): Promise<void> {
    try {
      const keys = await AsyncStorage.getAllKeys();
      const cacheKeys = keys.filter(key =>
        key.startsWith(this.cacheKeyPrefix)
      );
      await AsyncStorage.multiRemove(cacheKeys);
    } catch (error) {
      console.error('Cache clear error:', error);
    }
  }
}

// Performance monitoring
export function measurePerformance(name: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const start = Date.now();
      
      try {
        const result = await originalMethod.apply(this, args);
        const duration = Date.now() - start;
        
        console.log(`[Performance] ${name}: ${duration}ms`);
        
        return result;
      } catch (error) {
        const duration = Date.now() - start;
        console.error(`[Performance] ${name} failed after ${duration}ms`);
        throw error;
      }
    };

    return descriptor;
  };
}

Error Boundary

// src/components/ErrorBoundary.tsx
import React, { ErrorInfo, ReactNode } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  SafeAreaView,
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
    
    // Log to crash reporting service
    // crashlytics().recordError(error);
  }

  handleReset = () => {
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return <>{this.props.fallback}</>;
      }

      return (
        <SafeAreaView style={styles.container}>
          <View style={styles.content}>
            <Icon name="error-outline" size={80} color="#e74c3c" />
            <Text style={styles.title}>Oops! Something went wrong</Text>
            <Text style={styles.message}>
              {this.state.error?.message || 'An unexpected error occurred'}
            </Text>
            <TouchableOpacity style={styles.button} onPress={this.handleReset}>
              <Text style={styles.buttonText}>Try Again</Text>
            </TouchableOpacity>
          </View>
        </SafeAreaView>
      );
    }

    return this.props.children;
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  content: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333',
    marginTop: 20,
    marginBottom: 10,
  },
  message: {
    fontSize: 16,
    color: '#666',
    textAlign: 'center',
    marginBottom: 30,
  },
  button: {
    backgroundColor: '#3b5998',
    paddingVertical: 12,
    paddingHorizontal: 30,
    borderRadius: 25,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

export default ErrorBoundary;

Troubleshooting

Common Issues

  1. Deep Linking Not Working

    • Verify URL scheme in iOS Info.plist and Android manifest
    • Check AppDelegate.mm (iOS) and MainActivity (Android) for proper handling
    • Test with npx uri-scheme open "com.yourcompany.app://oauth/callback" --ios
  2. Token Refresh Failing

    • Ensure offline_access scope is included
    • Check refresh token expiration in Keycloak
    • Verify secure storage is working properly
  3. Android Release Build Issues

    • Add ProGuard rules for AppAuth and JWT libraries
    • Ensure manifest placeholders are set in build.gradle
    • Check for certificate pinning in release mode

Debug Utilities

// src/utils/debug.ts
import { LogBox } from 'react-native';

export class DebugUtils {
  static enableDebugMode() {
    if (__DEV__) {
      // Enable network debugging
      global.XMLHttpRequest = global.originalXMLHttpRequest || global.XMLHttpRequest;
      
      // Log all network requests
      const originalFetch = global.fetch;
      global.fetch = async (...args) => {
        console.log('[Network]', args[0], args[1]?.method || 'GET');
        const response = await originalFetch(...args);
        console.log('[Network Response]', response.status);
        return response;
      };
      
      // Ignore specific warnings
      LogBox.ignoreLogs([
        'Non-serializable values were found in the navigation state',
      ]);
    }
  }
  
  static logAuthState(authState: any) {
    console.log('=== Auth State ===');
    console.log('Authenticated:', authState.hasLoggedIn);
    console.log('Access Token:', authState.accessToken?.substring(0, 20) + '...');
    console.log('Expires At:', authState.accessTokenExpirationDate);
    console.log('User Info:', authState.userInfo);
  }
  
  static async testDeepLink(url: string) {
    const { Linking } = require('react-native');
    
    try {
      const canOpen = await Linking.canOpenURL(url);
      console.log(`Can open ${url}:`, canOpen);
      
      if (canOpen) {
        await Linking.openURL(url);
      }
    } catch (error) {
      console.error('Deep link test failed:', error);
    }
  }
}

Next Steps