← Volver al listado de tecnologías

Capítulo 1: Instalación y Configuración de AppsFlyer

Por: Artiko
appsflyerreact-nativeinstalaciónconfiguracióniosandroid

Capítulo 1: Instalación y Configuración de AppsFlyer

En este capítulo aprenderás a instalar y configurar correctamente AppsFlyer SDK en tu proyecto React Native, tanto para iOS como para Android.

🎯 Objetivos del Capítulo

Al finalizar este capítulo serás capaz de:

📚 Conceptos Fundamentales

¿Qué es la Atribución Móvil?

La atribución móvil es el proceso de identificar qué campaña de marketing, canal o touchpoint llevó a un usuario a instalar tu aplicación o realizar una acción específica.

Términos Clave

🔑 Prerrequisitos

Antes de comenzar, asegúrate de tener:

  1. Cuenta de AppsFlyer activa
  2. React Native 0.60 o superior
  3. CocoaPods instalado (para iOS)
  4. Android Studio y Xcode configurados

📦 Instalación del SDK

Paso 1: Instalar el Paquete NPM

# Con npm
npm install react-native-appsflyer --save

# Con yarn
yarn add react-native-appsflyer

Paso 2: Instalación Específica por Plataforma

iOS (con CocoaPods)

cd ios && pod install

Si encuentras problemas, intenta:

# Limpiar cache de pods
cd ios
pod deintegrate
pod cache clean --all
pod install

Android

Para Android con React Native 0.60+, la vinculación es automática. Sin embargo, verifica que tu android/build.gradle tenga:

buildscript {
    ext {
        minSdkVersion = 21 // Mínimo requerido para AppsFlyer
        compileSdkVersion = 33
        targetSdkVersion = 33
    }
}

🔧 Configuración de Credenciales

Obtener tus Credenciales de AppsFlyer

  1. Inicia sesión en tu dashboard de AppsFlyer
  2. Ve a App SettingsSDK Integration
  3. Copia tu Dev Key (igual para iOS y Android)
  4. Para iOS, copia tu Apple App ID (ejemplo: id123456789)
  5. Para Android, el package name es tu App ID

Guardar Credenciales de Forma Segura

Crea un archivo .env en la raíz de tu proyecto:

# .env
APPSFLYER_DEV_KEY=TuDevKeyAqui
APPSFLYER_IOS_APP_ID=id123456789
APPSFLYER_ANDROID_PACKAGE=com.tuapp.android

Instala react-native-dotenv:

npm install react-native-dotenv --save-dev

Configura Babel (babel.config.js):

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    ['module:react-native-dotenv', {
      moduleName: '@env',
      path: '.env',
      safe: false,
      allowUndefined: true,
    }]
  ]
};

📱 Configuración iOS

Paso 1: Modificar AppDelegate

Abre ios/YourApp/AppDelegate.m (Objective-C) o AppDelegate.swift (Swift):

Objective-C (AppDelegate.m)

#import <RNAppsFlyer.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  // ... código existente ...
  
  // Configurar AppsFlyer para deep linking
  [[AppsFlyerLib shared] setDelegate:self];
  
  return YES;
}

// Deep linking
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  [[AppsFlyerLib shared] handleOpenUrl:url options:options];
  return YES;
}

// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
  [[AppsFlyerLib shared] continueUserActivity:userActivity restorationHandler:restorationHandler];
  return YES;
}

@end

Swift (AppDelegate.swift)

import RNAppsFlyer

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // ... código existente ...
    
    // Configurar AppsFlyer para deep linking
    AppsFlyerLib.shared().delegate = self
    
    return true
  }
  
  // Deep linking
  func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    AppsFlyerLib.shared().handleOpen(url, options: options)
    return true
  }
  
  // Universal Links
  func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    AppsFlyerLib.shared().continue(userActivity, restorationHandler: restorationHandler)
    return true
  }
}

Paso 2: Configurar Info.plist

Añade las siguientes configuraciones en ios/YourApp/Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>tuapp</string> <!-- Tu esquema personalizado -->
    </array>
  </dict>
</array>

Paso 3: Configurar App Transport Security (si es necesario)

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
  <key>NSExceptionDomains</key>
  <dict>
    <key>appsflyer.com</key>
    <dict>
      <key>NSIncludesSubdomains</key>
      <true/>
      <key>NSExceptionAllowsInsecureHTTPLoads</key>
      <true/>
    </dict>
  </dict>
</dict>

🤖 Configuración Android

Paso 1: Modificar AndroidManifest.xml

Abre android/app/src/main/AndroidManifest.xml y añade:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tuapp">

    <!-- Permisos necesarios -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    
    <!-- Opcional: para Google Advertising ID -->
    <uses-permission android:name="com.google.android.gms.permission.AD_ID" />

    <application
        android:name=".MainApplication"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:allowBackup="false"
        android:theme="@style/AppTheme">
        
        <!-- Tu actividad principal -->
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
            android:windowSoftInputMode="adjustResize"
            android:launchMode="singleTask">
            
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            
            <!-- Deep linking -->
            <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="tuapp" />
            </intent-filter>
            
            <!-- App Links -->
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="https"
                      android:host="tuapp.onelink.me" />
            </intent-filter>
        </activity>
        
    </application>
</manifest>

Paso 2: Configurar ProGuard (si usas minificación)

En android/app/proguard-rules.pro:

# AppsFlyer
-keep class com.appsflyer.** { *; }
-keep class com.android.installreferrer.** { *; }
-keep class com.google.android.gms.common.ConnectionResult { *; }
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient { *; }
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info { *; }

Paso 3: Configurar MainActivity

En android/app/src/main/java/.../MainActivity.java:

package com.tuapp;

import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
import com.appsflyer.AppsFlyerLib;

public class MainActivity extends ReactActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
    }

    @Override
    protected String getMainComponentName() {
        return "TuApp";
    }
}

🧪 Primera Implementación

Crea archivos TypeScript con validación Zod:

// types/appsflyer.types.ts
import { z } from 'zod';

// Schema para opciones de inicialización
export const AppsFlyerInitOptionsSchema = z.object({
  devKey: z.string().min(1),
  isDebug: z.boolean(),
  appId: z.string().optional(),
  onInstallConversionDataListener: z.boolean(),
  onDeepLinkListener: z.boolean(),
  timeToWaitForATTUserAuthorization: z.number().optional(),
});

export type AppsFlyerInitOptions = z.infer<typeof AppsFlyerInitOptionsSchema>;

// Schema para datos de conversión
export const ConversionDataSchema = z.object({
  af_status: z.enum(['Organic', 'Non-organic']),
  media_source: z.string().optional(),
  campaign: z.string().optional(),
  is_first_launch: z.boolean(),
  af_click_lookback: z.string().optional(),
  install_time: z.string().optional(),
  af_sub1: z.string().optional(),
  af_sub2: z.string().optional(),
  af_sub3: z.string().optional(),
  af_sub4: z.string().optional(),
  af_sub5: z.string().optional(),
});

export type ConversionData = z.infer<typeof ConversionDataSchema>;

// Schema para deep link
export const DeepLinkDataSchema = z.object({
  deep_link_value: z.string().optional(),
  deep_link_sub1: z.string().optional(),
  campaign: z.string().optional(),
  media_source: z.string().optional(),
  is_deferred: z.boolean().optional(),
});

export type DeepLinkData = z.infer<typeof DeepLinkDataSchema>;

// Schema para eventos
export const EventParamsSchema = z.record(z.union([
  z.string(),
  z.number(),
  z.boolean(),
]));

export type EventParams = z.infer<typeof EventParamsSchema>;
// hooks/useAppsFlyer.ts
import { useEffect, useState, useCallback } from 'react';
import appsFlyer from 'react-native-appsflyer';
import { Platform } from 'react-native';
import { 
  AppsFlyerInitOptionsSchema,
  ConversionDataSchema,
  DeepLinkDataSchema,
  EventParamsSchema,
  type AppsFlyerInitOptions,
  type ConversionData,
  type DeepLinkData,
  type EventParams,
} from '../types/appsflyer.types';
import { 
  APPSFLYER_DEV_KEY, 
  APPSFLYER_IOS_APP_ID 
} from '@env';

interface UseAppsFlyerReturn {
  isInitialized: boolean;
  conversionData: ConversionData | null;
  deepLinkData: DeepLinkData | null;
  initAppsFlyer: () => Promise<void>;
  logEvent: (eventName: string, params?: EventParams) => Promise<void>;
  error: Error | null;
}

export const useAppsFlyer = (): UseAppsFlyerReturn => {
  const [isInitialized, setIsInitialized] = useState(false);
  const [conversionData, setConversionData] = useState<ConversionData | null>(null);
  const [deepLinkData, setDeepLinkData] = useState<DeepLinkData | null>(null);
  const [error, setError] = useState<Error | null>(null);

  const initAppsFlyer = useCallback(async () => {
    try {
      const initOptions: AppsFlyerInitOptions = {
        devKey: APPSFLYER_DEV_KEY,
        isDebug: __DEV__,
        appId: Platform.OS === 'ios' ? APPSFLYER_IOS_APP_ID : undefined,
        onInstallConversionDataListener: true,
        onDeepLinkListener: true,
        timeToWaitForATTUserAuthorization: 10,
      };

      // Validar opciones con Zod
      const validatedOptions = AppsFlyerInitOptionsSchema.parse(initOptions);

      await new Promise<void>((resolve, reject) => {
        appsFlyer.initSdk(
          validatedOptions,
          (result) => {
            console.log('✅ AppsFlyer inicializado:', result);
            setIsInitialized(true);
            resolve();
          },
          (error) => {
            console.error('❌ Error iniciando AppsFlyer:', error);
            reject(new Error(error));
          }
        );
      });
    } catch (err) {
      setError(err as Error);
      console.error('Error en inicialización:', err);
    }
  }, []);

  const logEvent = useCallback(async (eventName: string, params: EventParams = {}) => {
    if (!isInitialized) {
      console.warn('AppsFlyer no está inicializado');
      return;
    }

    try {
      const validatedParams = EventParamsSchema.parse(params);
      await appsFlyer.logEvent(eventName, validatedParams);
      console.log(`📊 Evento enviado: ${eventName}`, validatedParams);
    } catch (err) {
      console.error(`Error enviando evento ${eventName}:`, err);
    }
  }, [isInitialized]);

  useEffect(() => {
    if (!isInitialized) return;

    // Listener de datos de conversión
    const unsubscribeConversion = appsFlyer.onInstallConversionData((data) => {
      try {
        const validatedData = ConversionDataSchema.parse(data);
        setConversionData(validatedData);
        console.log('📊 Datos de conversión:', validatedData);
      } catch (err) {
        console.error('Error validando datos de conversión:', err);
      }
    });

    // Listener de deep links
    const unsubscribeDeepLink = appsFlyer.onDeepLink((data) => {
      try {
        const validatedData = DeepLinkDataSchema.parse(data);
        setDeepLinkData(validatedData);
        console.log('🔗 Deep Link:', validatedData);
      } catch (err) {
        console.error('Error validando deep link:', err);
      }
    });

    return () => {
      // Cleanup si es necesario
    };
  }, [isInitialized]);

  return {
    isInitialized,
    conversionData,
    deepLinkData,
    initAppsFlyer,
    logEvent,
    error,
  };
};

🚀 Integración en tu App

En tu componente principal (App.tsx):

import React, { useEffect } from 'react';
import { 
  View, 
  Text, 
  TouchableOpacity, 
  Alert, 
  StyleSheet,
  SafeAreaView 
} from 'react-native';
import { useAppsFlyer } from './hooks/useAppsFlyer';
import { z } from 'zod';

// Schema para props del componente
const AppPropsSchema = z.object({
  userId: z.string().optional(),
  environment: z.enum(['development', 'staging', 'production']).default('development'),
});

type AppProps = z.infer<typeof AppPropsSchema>;

const App: React.FC<AppProps> = (props) => {
  const validatedProps = AppPropsSchema.parse(props);
  const { 
    isInitialized, 
    conversionData, 
    deepLinkData, 
    initAppsFlyer, 
    logEvent, 
    error 
  } = useAppsFlyer();

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

  useEffect(() => {
    if (deepLinkData) {
      handleDeepLink(deepLinkData);
    }
  }, [deepLinkData]);

  const handleDeepLink = (linkData: typeof deepLinkData) => {
    if (!linkData) return;
    
    const { deep_link_value, campaign } = linkData;
    console.log('📱 Navegando a:', deep_link_value);
    
    // Aquí implementarías tu navegación
    // navigation.navigate(deep_link_value);
  };

  const sendTestEvent = async () => {
    try {
      await logEvent('test_event', {
        test_param: 'test_value',
        timestamp: Date.now(),
        environment: validatedProps.environment,
      });
      Alert.alert('✅ Éxito', 'Evento enviado al dashboard');
    } catch (err) {
      Alert.alert('❌ Error', 'No se pudo enviar el evento');
    }
  };

  const getStatusColor = () => {
    if (error) return '#FF5252';
    if (!isInitialized) return '#FFA726';
    return '#66BB6A';
  };

  const getStatusText = () => {
    if (error) return '❌ Error';
    if (!isInitialized) return '⏳ Iniciando...';
    return '✅ Inicializado';
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.content}>
        <Text style={styles.title}>AppsFlyer React Native</Text>
        
        <View style={[styles.statusCard, { borderColor: getStatusColor() }]}>
          <Text style={styles.statusLabel}>Estado:</Text>
          <Text style={[styles.statusText, { color: getStatusColor() }]}>
            {getStatusText()}
          </Text>
        </View>

        {error && (
          <View style={styles.errorCard}>
            <Text style={styles.errorText}>
              {error.message}
            </Text>
          </View>
        )}
        
        {conversionData && (
          <View style={styles.dataCard}>
            <Text style={styles.dataTitle}>📊 Datos de Conversión:</Text>
            <Text style={styles.dataLabel}>Estado: {conversionData.af_status}</Text>
            {conversionData.media_source && (
              <Text style={styles.dataLabel}>Fuente: {conversionData.media_source}</Text>
            )}
            {conversionData.campaign && (
              <Text style={styles.dataLabel}>Campaña: {conversionData.campaign}</Text>
            )}
          </View>
        )}

        {deepLinkData && (
          <View style={styles.dataCard}>
            <Text style={styles.dataTitle}>🔗 Deep Link:</Text>
            <Text style={styles.dataLabel}>{deepLinkData.deep_link_value}</Text>
          </View>
        )}
        
        <TouchableOpacity 
          style={[
            styles.button,
            !isInitialized && styles.buttonDisabled
          ]}
          onPress={sendTestEvent}
          disabled={!isInitialized}
        >
          <Text style={styles.buttonText}>Enviar Evento de Prueba</Text>
        </TouchableOpacity>
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  content: {
    flex: 1,
    padding: 20,
    justifyContent: 'center',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 30,
    color: '#333',
  },
  statusCard: {
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 10,
    borderWidth: 2,
    marginBottom: 20,
  },
  statusLabel: {
    fontSize: 14,
    color: '#666',
    marginBottom: 5,
  },
  statusText: {
    fontSize: 18,
    fontWeight: '600',
  },
  errorCard: {
    backgroundColor: '#FFEBEE',
    padding: 15,
    borderRadius: 10,
    marginBottom: 20,
  },
  errorText: {
    color: '#C62828',
    fontSize: 14,
  },
  dataCard: {
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 10,
    marginBottom: 20,
  },
  dataTitle: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 10,
    color: '#333',
  },
  dataLabel: {
    fontSize: 14,
    color: '#666',
    marginBottom: 5,
  },
  button: {
    backgroundColor: '#00C853',
    padding: 15,
    borderRadius: 10,
    alignItems: 'center',
  },
  buttonDisabled: {
    backgroundColor: '#BDBDBD',
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
});

export default App;

🧪 Testing y Verificación

1. Modo Debug

Asegúrate de que el modo debug esté activado durante el desarrollo:

const initOptions = {
  devKey: APPSFLYER_DEV_KEY,
  isDebug: true, // Activar logs detallados
  // ...
};

2. Verificar en Logs

iOS

# Ver logs en Xcode Console
# Busca mensajes que empiecen con [AppsFlyerSDK]

Android

# Ver logs con adb
adb logcat | grep "AppsFlyer"

3. AppsFlyer SDK Integration Test App

  1. Descarga la app de test desde App Store/Play Store
  2. Ingresa tu Dev Key
  3. Verifica que los eventos aparezcan

4. Dashboard de AppsFlyer

  1. Ve a tu dashboard en hq1.appsflyer.com
  2. Navega a Real-TimeOverview
  3. Los eventos deberían aparecer en ~30 segundos

🐛 Troubleshooting Común

Problema: “SDK not initialized”

Solución:

// Asegúrate de esperar la inicialización
await AppsFlyerService.init();
// Luego envía eventos

Problema: No aparecen eventos en el dashboard

Checklist:

Problema: Build falla en iOS

cd ios
rm -rf Pods Podfile.lock
pod install --repo-update

iOS: Verifica URL schemes en Info.plist Android: Verifica intent-filters en AndroidManifest.xml

📋 Checklist de Instalación

🎯 Ejercicios Prácticos

Ejercicio 1: Primera Instalación

  1. Desinstala tu app del dispositivo
  2. Reinstala y abre la app
  3. Verifica que aparezca como “Nueva Instalación” en el dashboard

Ejercicio 2: Evento Personalizado

// Implementa este evento con validación
const TutorialEventSchema = z.object({
  tutorial_name: z.string(),
  duration: z.number().positive(),
  success: z.boolean(),
  steps_completed: z.number().optional(),
});

type TutorialEvent = z.infer<typeof TutorialEventSchema>;

const sendTutorialEvent = async (data: TutorialEvent) => {
  try {
    const validatedData = TutorialEventSchema.parse(data);
    await logEvent('tutorial_completed', validatedData);
  } catch (error) {
    if (error instanceof z.ZodError) {
      console.error('Validación fallida:', error.errors);
    }
  }
};

// Uso
sendTutorialEvent({
  tutorial_name: 'appsflyer_setup',
  duration: 300,
  success: true,
  steps_completed: 5,
});
  1. Crea un deep link: tuapp://product/123
  2. Ábrelo desde Safari/Chrome
  3. Verifica que tu app lo reciba correctamente

📚 Resumen

En este capítulo aprendiste:

🚀 Próximo Capítulo

En el siguiente capítulo profundizaremos en la Inicialización y Configuración Avanzada, incluyendo:


← Volver al Índice | Siguiente: Inicialización Básica →