✓
Decisión de enfoque: Integrado
Análisis de criterios aplicados a FinTrack
Enfoque elegido: Integrado (Integrated)
FinTrack cumple todos los criterios para el enfoque integrado: un único equipo propietario del código nativo y RN, repositorio monorepo con ios/ y android/ en la raíz, y el equipo acepta aprender RN y añadir las herramientas al pipeline de CI/CD. El beneficio principal es hot reload + source maps desde el primer día.
| Criterio | Aislado | Integrado | FinTrack |
|---|---|---|---|
| Equipo nativo acepta tooling RN | ✗ No requerido | ✓ Requerido | ✓ Sí |
| Repositorio único / monorepo | ✗ Repos separados | ✓ Monorepo | ✓ Monorepo |
| Hot reload + devtools | Parcial (Metro externo) | ✓ Nativo en build | ✓ Prioritario |
| Build system estándar | ✓ Ningún cambio | Gradle + CocoaPods | ✓ Ya usa ambos |
| Muchos módulos Expo | Rebuild artefacto cada vez | ✓ Autolinking | ✓ Previsto |
| Un pipeline CI/CD | Dos pipelines | ✓ Pipeline único | ✓ Requerido |
◈
Arquitectura resultante
Monorepo FinTrack con Expo SDK 55 integrado
ESTRUCTURA DEL MONOREPO
fintrack/ ← raíz del monorepo ├── package.json ← workspaces: ["fintrack-rn"] ├── fintrack-rn/ ← proyecto Expo SDK 55 │ ├── app.json │ ├── package.json │ ├── app/ │ │ ├── InversionesOnboarding/ ← 5 pantallas RN │ │ └── _layout.tsx │ └── node_modules/ ├── android/ ← app Android nativa existente │ ├── settings.gradle ← + RN Gradle plugin + autolinking │ ├── build.gradle │ └── app/ │ ├── build.gradle ← + plugin com.facebook.react │ ├── src/main/ │ │ └── java/com/fintrack/ │ │ ├── MainApplication.kt ← ExpoReactHostFactory │ │ └── InversionesActivity.kt ← ReactActivity │ └── ... └── ios/ ← app iOS nativa existente ├── Podfile ← + use_expo_modules! + use_react_native! ├── Podfile.properties.json └── FinTrack.xcworkspace/ ← abrir SIEMPRE este └── AppDelegate.swift ← ExpoReactNativeFactory
La app nativa existente (iOS + Android) permanece intacta. Solo se añaden los archivos de configuración marcados arriba. El código RN vive en fintrack-rn/ y comparte el AppRegistry.registerComponent("main", ...) entre plataformas.
≡
Pasos de integración
Secuencia completa para FinTrack
1
Crear el proyecto Expo (SDK 55+)
Terminal — desde la raíz del monorepo
shell
# Crear el proyecto Expo dentro del monorepo npx create-expo-app@latest fintrack-rn \ --template default@sdk-55 # Registrar el componente con el nombre que usará el lado nativo # En fintrack-rn/app/_layout.tsx (ya incluido en el template): # AppRegistry.registerComponent("main", () => App) # Instalar dependencias cd fintrack-rn && yarn install
SDK mínimo 55. Versiones anteriores no incluyen expo-brownfield, ExpoReactHostFactory ni ExpoReactNativeFactory. Fijar explícitamente el SDK con --template default@sdk-55.
2
Configurar el workspace en la raíz del monorepo
package.json (raíz)
json
{
"name": "fintrack-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": ["fintrack-rn"]
}
Después ejecutar yarn install en la raíz para que node_modules quede en el nivel workspace y tanto Gradle como CocoaPods puedan resolver las dependencias de React Native y Expo.
3
Configurar Android — Gradle
🤖 Android
android/settings.gradle
groovy
// Registrar RN Gradle Plugin + Expo autolinking pluginManagement { def reactNativeGradlePlugin = new File( providers.exec { workingDir(rootDir) commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })") }.standardOutput.asText.get().trim() ).getParentFile().absolutePath includeBuild(reactNativeGradlePlugin) // Monorepo: node_modules está en fintrack-rn/node_modules def expoPluginsPath = new File( providers.exec { workingDir(rootDir) commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })") }.standardOutput.asText.get().trim(), "../android/expo-gradle-plugin" ).absolutePath includeBuild(expoPluginsPath) } plugins { id("com.facebook.react.settings") id("expo-autolinking-settings") } extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand) } expoAutolinking.useExpoModules() expoAutolinking.useExpoVersionCatalog() includeBuild(expoAutolinking.reactNativeGradlePlugin)
android/app/build.gradle — bloque react {}
groovy
apply plugin: "com.android.application" apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" // Monorepo: projectRoot apunta al proyecto Expo def projectRoot = "$rootDir/../fintrack-rn" react { entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim()) reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) bundleCommand = "export:embed" root = file("../../") // raíz del monorepo autolinkLibrariesWithApp() }
MainApplication.kt
kotlin
package com.fintrack import android.app.Application import android.content.res.Configuration import com.facebook.react.PackageList import com.facebook.react.ReactApplication import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative import com.facebook.react.ReactHost import expo.modules.ApplicationLifecycleDispatcher import expo.modules.ExpoReactHostFactory class MainApplication : Application(), ReactApplication { override val reactHost: ReactHost by lazy { ExpoReactHostFactory.getDefaultReactHost( context = applicationContext, packageList = PackageList(this).packages ) } override fun onCreate() { super.onCreate() loadReactNative(this) ApplicationLifecycleDispatcher.onApplicationCreate(this) } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) } }
InversionesActivity.kt — lanza el flujo RN
kotlin
package com.fintrack import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate import expo.modules.ReactActivityDelegateWrapper /** * Activity que aloja el flujo InversionesOnboarding en React Native. * Lanzar desde HomeActivity con startActivity(Intent(this, InversionesActivity::class.java)) */ class InversionesActivity : ReactActivity() { // Debe coincidir con AppRegistry.registerComponent("main", ...) en JS override fun getMainComponentName(): String = "main" override fun createReactActivityDelegate(): ReactActivityDelegate { return ReactActivityDelegateWrapper( this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, object : DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) {} ) } }
AndroidManifest.xml — registrar la Activity
xml
<!-- Permiso para Metro en debug --> <uses-permission android:name="android.permission.INTERNET" /> <!-- Registrar InversionesActivity (sin ActionBar para RN) --> <activity android:name=".InversionesActivity" android:theme="@style/Theme.AppCompat.Light.NoActionBar" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" />
android/gradle.properties
properties
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 newArchEnabled=true hermesEnabled=true
4
Configurar iOS — CocoaPods + Xcode
🍎 iOS
ios/Podfile
ruby
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
platform :ios, podfile_properties['ios.deploymentTarget'] || '16.4'
prepare_react_native_project!
target 'FinTrack' do # ← nombre del target Xcode existente
use_expo_modules!
config_command = [
'node', '--no-warnings', '--eval',
"require('expo/bin/autolinking')",
'expo-modules-autolinking', 'react-native-config',
'--json', '--platform', 'ios'
]
config = use_native_modules!(config_command)
use_react_native!(
path: config[:reactNativePath],
hermes_enabled: podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
# Monorepo: ruta al proyecto Expo
app_path: "#{Pod::Config.instance.installation_root}/../fintrack-rn",
privacy_file_aggregation_enabled: podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
post_install do |installer|
react_native_post_install(installer, config[:reactNativePath], mac_catalyst_enabled: false)
end
end
ios/Podfile.properties.json
json
{
"expo.jsEngine": "hermes",
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
"ios.deploymentTarget": "16.4"
}
AppDelegate.swift — integrar ExpoReactNativeFactory
swift
internal import Expo import React import ReactAppDependencyProvider @main class AppDelegate: ExpoAppDelegate { var window: UIWindow? var reactNativeDelegate: ExpoReactNativeFactoryDelegate? var reactNativeFactory: RCTReactNativeFactory? public override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { let delegate = ReactNativeDelegate() let factory = ExpoReactNativeFactory(delegate: delegate) delegate.dependencyProvider = RCTAppDependencyProvider() reactNativeDelegate = delegate reactNativeFactory = factory window = UIWindow(frame: UIScreen.main.bounds) factory.startReactNative( withModuleName: "main", // debe coincidir con AppRegistry.registerComponent in: window, launchOptions: launchOptions ) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { override func bundleURL() -> URL? { #if DEBUG return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") #else return Bundle.main.url(forResource: "main", withExtension: "jsbundle") #endif } }
3 cambios obligatorios en Xcode:
- ENABLE_USER_SCRIPT_SANDBOXING → No (Build Settings) — los scripts de Hermes lo necesitan.
- Run Script phase antes de [CP] Embed Pods Frameworks — genera main.jsbundle en release.
- Info.plist: UIViewControllerBasedStatusBarAppearance → NO.
Terminal — instalar pods
shell
# Desde la raíz del monorepo EXPO_PROJECT_ROOT=$(pwd)/fintrack-rn pod install --project-directory=ios # Abrir workspace (NO el .xcodeproj) open ios/FinTrack.xcworkspace
5
Probar la integración
Arrancar Metro + build nativo
shell
# 1. Arrancar Metro desde el proyecto Expo cd fintrack-rn && yarn start # 2. En otra terminal — Android debug cd android && ./gradlew installDebug # 3. iOS — compilar desde Xcode o CLI xcodebuild -workspace ios/FinTrack.xcworkspace \ -scheme FinTrack \ -configuration Debug \ -destination 'platform=iOS Simulator,name=iPhone 16' # 4. Navegar a InversionesActivity/VC desde la app nativa # → El flujo React Native carga con hot reload activo ✓
Checklist de validación
- ✓ Metro arranca sin errores (yarn start)
- ✓ Android: ./gradlew installDebug compila OK
- ✓ iOS: pod install completa sin conflictos de versión
- ✓ InversionesActivity lanza el flujo RN desde HomeActivity
- ✓ Hot reload funciona al editar app/InversionesOnboarding/
- ✓ Release build embebe main.jsbundle correctamente
- ○ CI/CD: añadir yarn start --non-interactive + export:embed al pipeline
Integración completada. El flujo InversionesOnboarding corre en React Native/Expo dentro de la app nativa existente de FinTrack. El código JS en fintrack-rn/app/InversionesOnboarding/ se puede iterar con hot reload sin tocar el código nativo.