← Volver al listado de tecnologías

Capítulo 7: Configuración de Clerk - Autenticación Moderna

Por: Artiko
react-nativeclerkautenticaciónoauthtypescripttesting

Capítulo 7: Configuración de Clerk - Autenticación Moderna

En este capítulo configuraremos Clerk, una plataforma de autenticación moderna que nos proporcionará un sistema completo de autenticación con OAuth, verificación de email, gestión de usuarios y mucho más.

🎯 Objetivos del Capítulo

📋 Prerrequisitos

🚀 Configuración de Cuenta Clerk

Paso 1: Crear Cuenta en Clerk

  1. Ve a clerk.com y crea una cuenta gratuita
  2. Crea una nueva aplicación llamada “TodoApp”
  3. Selecciona “React Native” como plataforma
  4. Anota las claves que aparecen (las necesitaremos más adelante)

Paso 2: Configuración Inicial en Clerk Dashboard

// Configuración que haremos en el dashboard:
// 1. Enable Email/Password authentication
// 2. Enable Google OAuth
// 3. Enable Apple OAuth (para iOS)
// 4. Configure redirect URLs
// 5. Set up email templates

📦 Instalación de Dependencias

Instalemos las dependencias necesarias para Clerk:

# Instalar Clerk para Expo
npx expo install @clerk/clerk-expo

# Instalar dependencias para OAuth
npx expo install expo-linking expo-auth-session expo-crypto

# Instalar Web Browser para OAuth flows
npx expo install expo-web-browser

# Instalar Secure Store para tokens
npx expo install expo-secure-store

Actualicemos nuestro package.json con las nuevas dependencias:

{
  "dependencies": {
    "@clerk/clerk-expo": "^0.19.14",
    "expo-linking": "~6.2.2",
    "expo-auth-session": "~5.4.0",
    "expo-crypto": "~12.8.1",
    "expo-web-browser": "~12.8.2",
    "expo-secure-store": "~12.9.0"
  }
}

🔧 Configuración de Variables de Entorno

Creemos nuestro archivo de configuración de entorno:

// .env.local
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here

# Para desarrollo
CLERK_SECRET_KEY=sk_test_your_secret_key_here

# URLs de redirect para OAuth
EXPO_PUBLIC_REDIRECT_URL=exp://localhost:19000/--/oauth-callback

Actualicemos nuestro app.json para incluir el esquema de URL:

{
  "expo": {
    "name": "TodoApp",
    "slug": "todo-app",
    "scheme": "todoapp",
    "platforms": ["ios", "android"],
    "extra": {
      "clerkPublishableKey": process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY
    }
  }
}

🏗️ Configuración del ClerkProvider

Creemos el proveedor principal de Clerk:

// src/providers/ClerkProvider.tsx
import React from 'react';
import { ClerkProvider as BaseClerkProvider } from '@clerk/clerk-expo';
import Constants from 'expo-constants';
import * as SecureStore from 'expo-secure-store';

// Configuración de token cache
const tokenCache = {
  async getToken(key: string) {
    try {
      return SecureStore.getItemAsync(key);
    } catch (err) {
      return null;
    }
  },
  async saveToken(key: string, value: string) {
    try {
      return SecureStore.setItemAsync(key, value);
    } catch (err) {
      return;
    }
  },
};

interface ClerkProviderProps {
  children: React.ReactNode;
}

export const ClerkProvider: React.FC<ClerkProviderProps> = ({ children }) => {
  const publishableKey = Constants.expoConfig?.extra?.clerkPublishableKey;

  if (!publishableKey) {
    throw new Error(
      'Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env'
    );
  }

  return (
    <BaseClerkProvider
      publishableKey={publishableKey}
      tokenCache={tokenCache}
    >
      {children}
    </BaseClerkProvider>
  );
};

🔐 Configuración de OAuth Providers

Google OAuth Setup

// src/config/oauth.ts
export const OAuthConfig = {
  google: {
    iosClientId: 'your-ios-client-id.googleusercontent.com',
    androidClientId: 'your-android-client-id.googleusercontent.com',
    webClientId: 'your-web-client-id.googleusercontent.com',
  },
  apple: {
    clientId: 'com.yourcompany.todoapp',
    redirectUri: 'https://your-app.clerk.accounts.dev/oauth_callback',
  },
} as const;

Configuración en Google Cloud Console

// Pasos para configurar Google OAuth:
// 1. Ir a Google Cloud Console
// 2. Crear nuevo proyecto o seleccionar existente
// 3. Habilitar Google+ API
// 4. Crear credenciales OAuth 2.0
// 5. Configurar pantalla de consentimiento
// 6. Agregar redirect URIs de Clerk

🏪 Integración con Zustand

Actualicemos nuestro store para incluir el estado de autenticación:

// src/stores/authStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { User } from '@clerk/clerk-expo';

interface AuthState {
  // Estado de autenticación
  isAuthenticated: boolean;
  user: User | null;
  isLoading: boolean;
  
  // Acciones
  setUser: (user: User | null) => void;
  setLoading: (loading: boolean) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  devtools(
    persist(
      immer((set) => ({
        // Estado inicial
        isAuthenticated: false,
        user: null,
        isLoading: true,

        // Acciones
        setUser: (user) =>
          set((state) => {
            state.user = user;
            state.isAuthenticated = !!user;
            state.isLoading = false;
          }),

        setLoading: (loading) =>
          set((state) => {
            state.isLoading = loading;
          }),

        logout: () =>
          set((state) => {
            state.user = null;
            state.isAuthenticated = false;
            state.isLoading = false;
          }),
      })),
      {
        name: 'auth-storage',
        storage: {
          getItem: async (name) => {
            const value = await AsyncStorage.getItem(name);
            return value ? JSON.parse(value) : null;
          },
          setItem: async (name, value) => {
            await AsyncStorage.setItem(name, JSON.stringify(value));
          },
          removeItem: async (name) => {
            await AsyncStorage.removeItem(name);
          },
        },
        partialize: (state) => ({
          user: state.user,
          isAuthenticated: state.isAuthenticated,
        }),
      }
    ),
    { name: 'auth-store' }
  )
);

🎣 Hook Personalizado para Autenticación

Creemos un hook que combine Clerk con nuestro store:

// src/hooks/useAuth.ts
import { useEffect } from 'react';
import { useUser, useAuth as useClerkAuth } from '@clerk/clerk-expo';
import { useAuthStore } from '../stores/authStore';

export const useAuth = () => {
  const { user, isLoaded } = useUser();
  const { signOut } = useClerkAuth();
  const { setUser, setLoading, logout, ...authState } = useAuthStore();

  useEffect(() => {
    if (isLoaded) {
      setUser(user);
    } else {
      setLoading(true);
    }
  }, [user, isLoaded, setUser, setLoading]);

  const handleLogout = async () => {
    try {
      await signOut();
      logout();
    } catch (error) {
      console.error('Error during logout:', error);
    }
  };

  return {
    ...authState,
    user,
    isLoaded,
    logout: handleLogout,
  };
};

🧪 Configuración de Testing con Clerk

Configuremos mocks para testing con Clerk:

// __mocks__/@clerk/clerk-expo.ts
export const useUser = jest.fn(() => ({
  user: null,
  isLoaded: true,
}));

export const useAuth = jest.fn(() => ({
  isLoaded: true,
  isSignedIn: false,
  signOut: jest.fn(),
}));

export const ClerkProvider = ({ children }: { children: React.ReactNode }) => {
  return <>{children}</>;
};

export const SignedIn = ({ children }: { children: React.ReactNode }) => {
  return <>{children}</>;
};

export const SignedOut = ({ children }: { children: React.ReactNode }) => {
  return <>{children}</>;
};

Utilidades de Testing

// src/utils/test-utils.tsx
import React from 'react';
import { render, RenderOptions } from '@testing-library/react-native';
import { ClerkProvider } from '../providers/ClerkProvider';

// Mock del Constants para testing
jest.mock('expo-constants', () => ({
  expoConfig: {
    extra: {
      clerkPublishableKey: 'pk_test_mock_key',
    },
  },
}));

const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
  return <ClerkProvider>{children}</ClerkProvider>;
};

const customRender = (
  ui: React.ReactElement,
  options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllTheProviders, ...options });

export * from '@testing-library/react-native';
export { customRender as render };

🧪 Tests para la Configuración

// src/hooks/__tests__/useAuth.test.ts
import { renderHook } from '@testing-library/react-native';
import { useAuth } from '../useAuth';
import { useUser, useAuth as useClerkAuth } from '@clerk/clerk-expo';

jest.mock('@clerk/clerk-expo');

const mockedUseUser = useUser as jest.MockedFunction<typeof useUser>;
const mockedUseClerkAuth = useClerkAuth as jest.MockedFunction<typeof useClerkAuth>;

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

  it('should return initial state when not loaded', () => {
    mockedUseUser.mockReturnValue({
      user: null,
      isLoaded: false,
    });
    mockedUseClerkAuth.mockReturnValue({
      signOut: jest.fn(),
    });

    const { result } = renderHook(() => useAuth());

    expect(result.current.isLoading).toBe(true);
    expect(result.current.isAuthenticated).toBe(false);
    expect(result.current.user).toBe(null);
  });

  it('should update state when user is loaded', () => {
    const mockUser = {
      id: 'user_123',
      emailAddresses: [{ emailAddress: '[email protected]' }],
      firstName: 'John',
      lastName: 'Doe',
    };

    mockedUseUser.mockReturnValue({
      user: mockUser,
      isLoaded: true,
    });
    mockedUseClerkAuth.mockReturnValue({
      signOut: jest.fn(),
    });

    const { result } = renderHook(() => useAuth());

    expect(result.current.isLoading).toBe(false);
    expect(result.current.isAuthenticated).toBe(true);
    expect(result.current.user).toBe(mockUser);
  });

  it('should handle logout correctly', async () => {
    const mockSignOut = jest.fn();
    mockedUseClerkAuth.mockReturnValue({
      signOut: mockSignOut,
    });
    mockedUseUser.mockReturnValue({
      user: null,
      isLoaded: true,
    });

    const { result } = renderHook(() => useAuth());

    await result.current.logout();

    expect(mockSignOut).toHaveBeenCalled();
  });
});

🎨 Componente de Estado de Autenticación

Creemos un componente para mostrar el estado de autenticación:

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

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

  if (isLoading) {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>Cargando autenticación...</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.text}>
        Estado: {isAuthenticated ? 'Autenticado' : 'No autenticado'}
      </Text>
      {user && (
        <Text style={styles.userText}>
          Usuario: {user.firstName} {user.lastName}
        </Text>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    padding: 16,
    backgroundColor: '#f5f5f5',
    borderRadius: 8,
    margin: 16,
  },
  text: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 8,
  },
  userText: {
    fontSize: 14,
    color: '#666',
  },
});

🔧 Actualización del App Principal

Actualicemos nuestro componente principal para incluir Clerk:

// App.tsx
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { ClerkProvider } from './src/providers/ClerkProvider';
import { MainNavigator } from './src/navigation/MainNavigator';
import { AuthStatus } from './src/components/AuthStatus';

export default function App() {
  return (
    <SafeAreaProvider>
      <ClerkProvider>
        <AuthStatus />
        <MainNavigator />
      </ClerkProvider>
    </SafeAreaProvider>
  );
}

📱 Testing en Dispositivo

Para probar la configuración:

# Ejecutar en desarrollo
npx expo start

# Verificar que no hay errores de configuración
# El componente AuthStatus debería mostrar "No autenticado"

# Para testing en dispositivo real
npx expo start --tunnel

🔍 Verificación de la Configuración

Checklist para verificar que todo está configurado correctamente:

📝 Configuración de Desarrollo vs Producción

// src/config/environment.ts
import Constants from 'expo-constants';

export const Environment = {
  isDevelopment: __DEV__,
  clerkPublishableKey: Constants.expoConfig?.extra?.clerkPublishableKey,
  
  // URLs base según entorno
  apiUrl: __DEV__ 
    ? 'http://localhost:3000/api'
    : 'https://your-api.com/api',
    
  // Configuración de OAuth según entorno
  oauthRedirectUrl: __DEV__
    ? 'exp://localhost:19000/--/oauth-callback'
    : 'todoapp://oauth-callback',
} as const;

🚀 Siguientes Pasos

En el próximo capítulo implementaremos:

📚 Recursos Adicionales

🎯 Resumen del Capítulo

Hemos establecido una base sólida para la autenticación en nuestra aplicación:

Configuración completa de ClerkVariables de entorno y configuraciónIntegración con Zustand para estado globalHook personalizado para autenticaciónTesting configurado con mocksComponentes de estado de autenticación

La aplicación ahora tiene una infraestructura de autenticación robusta y lista para implementar los flujos de login y registro en el siguiente capítulo.


Próximo capítulo: Implementación de Autenticación - Crearemos los componentes de SignIn, SignUp y flujos OAuth completos.

🔗 Navegación