Capítulo 1: Instalación y Configuración de AppsFlyer
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:
- ✅ Entender los conceptos básicos de atribución móvil
- ✅ Instalar AppsFlyer SDK en tu proyecto
- ✅ Configurar el SDK para iOS y Android
- ✅ Obtener y configurar las credenciales necesarias
- ✅ Realizar la primera prueba de integración
📚 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
- Install Attribution: Identificar la fuente de una instalación
- Dev Key: Clave única de desarrollador para tu cuenta AppsFlyer
- App ID: Identificador único de tu aplicación
- Conversion Data: Datos sobre la fuente de conversión
- Deep Link: Enlaces que llevan a contenido específico en tu app
- Organic vs Non-Organic: Instalaciones naturales vs campañas pagas
🔑 Prerrequisitos
Antes de comenzar, asegúrate de tener:
- Cuenta de AppsFlyer activa
- React Native 0.60 o superior
- CocoaPods instalado (para iOS)
- 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
- Inicia sesión en tu dashboard de AppsFlyer
- Ve a App Settings → SDK Integration
- Copia tu Dev Key (igual para iOS y Android)
- Para iOS, copia tu Apple App ID (ejemplo: id123456789)
- 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
- Descarga la app de test desde App Store/Play Store
- Ingresa tu Dev Key
- Verifica que los eventos aparezcan
4. Dashboard de AppsFlyer
- Ve a tu dashboard en hq1.appsflyer.com
- Navega a Real-Time → Overview
- 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:
- ✓ Dev Key correcta
- ✓ App ID correcto (iOS)
- ✓ Modo debug activado
- ✓ Dispositivo con conexión a internet
- ✓ No usar VPN durante testing
Problema: Build falla en iOS
cd ios
rm -rf Pods Podfile.lock
pod install --repo-update
Problema: Deep links no funcionan
iOS: Verifica URL schemes en Info.plist Android: Verifica intent-filters en AndroidManifest.xml
📋 Checklist de Instalación
- SDK instalado vía npm/yarn
- Pods instalados (iOS)
- Dev Key configurada
- App ID configurado (iOS)
- AppDelegate modificado (iOS)
- AndroidManifest configurado
- Modo debug activado para testing
- Primer evento enviado exitosamente
- Evento visible en dashboard
🎯 Ejercicios Prácticos
Ejercicio 1: Primera Instalación
- Desinstala tu app del dispositivo
- Reinstala y abre la app
- 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,
});
Ejercicio 3: Debug Deep Link
- Crea un deep link:
tuapp://product/123
- Ábrelo desde Safari/Chrome
- Verifica que tu app lo reciba correctamente
📚 Resumen
En este capítulo aprendiste:
- ✅ Conceptos básicos de atribución móvil
- ✅ Cómo instalar AppsFlyer SDK
- ✅ Configuración para iOS y Android
- ✅ Implementación básica del SDK
- ✅ Testing y verificación
🚀 Próximo Capítulo
En el siguiente capítulo profundizaremos en la Inicialización y Configuración Avanzada, incluyendo:
- Gestión de permisos (ATT en iOS 14.5+)
- Configuración de User ID
- Opt-out y privacy
- Configuración para diferentes entornos