Quando ho iniziato a lavorare su myGenius — un’app aziendale per la gestione presenze, ferie e timbrature NFC — la prima decisione da prendere è stata: Expo o bare workflow?
Avevamo bisogno di accesso diretto all’hardware NFC, integrazione con Keychain per i token JWT, Firebase Cloud Messaging per le notifiche push, e una WebView come core applicativo. Expo, per quanto sia migliorato tantissimo negli ultimi anni, aggiungeva un livello di astrazione che non volevamo. Volevamo il controllo totale sul processo di build, sulle dipendenze native, su ogni singolo flag di compilazione.
In questo articolo ti mostro come ho configurato il progetto myGenius da zero: tutti i file reali, le versioni specifiche, e le motivazioni dietro ogni scelta. Non è un tutorial generico — è il setup di un’app che oggi è in produzione sugli store, con oltre 40 release pubblicate.
Prerequisiti: cosa ti serve prima di iniziare
Prima di toccare un solo file, devi avere l’ambiente di sviluppo pronto. React Native bare workflow richiede strumenti specifici su macOS (per compilare iOS) e su qualsiasi sistema per Android.
Node.js
React Native 0.78.1 richiede Node 18 o superiore. Lo specifichiamo direttamente nel package.json:
{
"engines": {
"node": ">=18"
}
}
Usa nvm o volta per gestire le versioni. Io consiglio Node 20 LTS per stabilità.
Ruby
iOS ha bisogno di Ruby per CocoaPods. Il nostro Gemfile specifica:
source 'https://rubygems.org'
ruby ">= 2.6.10"
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.4'
Noterai che escludiamo le versioni 1.15.0 e 1.15.1 di CocoaPods. Hanno un bug noto che rompe la build di progetti React Native. activesupport 7.1.0 ha un’incompatibilità con CocoaPods. xcodeproj oltre 1.26.0 introduce breaking changes. concurrent-ruby oltre 1.3.4 causa problemi di risoluzione delle dipendenze.
Questi non sono capricci. Ogni esclusione ha un nome e un cognome: la 1.15.0 di CocoaPods mi ha bloccato per un pomeriggio intero prima che capissi cosa stava succedendo. concurrent-ruby 1.3.4 è spuntato fuori dopo un aggiornamento di Ruby che nessuno aveva richiesto. Sono ore di build fallite che mi hanno insegnato esattamente quali versioni evitare.
Android SDK
Per Android servono:
- Build Tools 35.0.0
- compileSdk 35
- minSdk 26
- targetSdk 35
- NDK 27.1.12297006
- Kotlin 2.0.21
Tutto questo è dichiarato nel build.gradle di progetto:
buildscript {
ext {
buildToolsVersion = "35.0.0"
minSdkVersion = 26
compileSdkVersion = 35
targetSdkVersion = 35
ndkVersion = "27.1.12297006"
kotlinVersion = "2.0.21"
}
}
minSdk 26 significa Android 8.0. È un limite ragionevole nel 2026: copre oltre il 95% dei dispositivi attivi e ci permette di usare API moderne senza troppi workaround.
Xcode
Per iOS serve Xcode (ultima versione). Il Podfile usa la sintassi moderna che delega a React Native la gestione della versione minima:
platform :ios, min_ios_version_supported
React Native 0.78.x imposta automaticamente la versione minima iOS supportata. Se hai bisogno di un limite diverso, puoi sostituire min_ios_version_supported con un numero esplicito (es. 15.1 o 16.0).
Creazione del progetto: react-native init vs Expo
La creazione del progetto è un singolo comando:
npx @react-native-community/cli@latest init myGenius
Questo genera la struttura base con android/ e ios/ nativi. Niente Expo, niente expo-go, niente prebuild. Tu e il compilatore siete faccia a faccia.
La differenza con Expo è che ora sei responsabile di:
mygenius-logo
- Aggiornare i pod iOS manualmente (
bundle exec pod install) - Configurare il
build.gradleper Android - Gestire i certificati di firma (noi usiamo fastlane con
match, ma è un argomento per un altro articolo) - Risolvere i conflitti di dipendenze native
In cambio ottieni: accesso completo a tutte le API native, nessun limite di plugin, build più veloci in CI/CD, e un controllo granulare che Expo non ti dà.
Per myGenius valeva la pena. Per un prototipo, una landing page o un’app con funzionalità basic? Expo probabilmente ti fa risparmiare giorni. La scelta dipende da cosa devi costruire, non da cosa è “meglio” in assoluto.
Configurazione di Metro e babel.config.js
Metro
Metro è il bundler di React Native. La configurazione di base è minimale:
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
const { withNativeWind } = require("nativewind/metro");
const config = mergeConfig(getDefaultConfig(__dirname), {
/* your config */
});
module.exports = withNativeWind(config, { input: "./global.css" });
Due cose da notare. Primo, usiamo getDefaultConfig e la fondiamo con la nostra configurazione. Non sovrascriviamo nulla di ciò che React Native già gestisce. Secondo, withNativeWind avvolge la config per abilitare NativeWind (che vedremo tra poco).
Se avessi dovuto aggiungere configurazioni custom — per esempio resolver.blockList per escludere file, o transformer personalizzati — sarebbero andate nell’oggetto vuoto commentato con /* your config */.
babel.config.js
Il file babel.config.js è dove la magia del nostro progetto prende forma:
require('dotenv').config({ path: require('path').resolve(__dirname, '.env') });
const useLocal = process.env.USE_LOCAL_PACKAGES === 'true';
module.exports = {
presets: ['module:@react-native/babel-preset', 'nativewind/babel'],
plugins: [
[
'module:react-native-dotenv',
{
moduleName: '@env',
path: '.env',
},
],
...(useLocal
? [
[
'module-resolver',
{
root: ['./'],
alias: {
'@geniuswebsrl/mobile-ui': './src/packages/mobile-ui/src',
'@geniuswebsrl/auth': './src/packages/auth/src',
'@geniuswebsrl/api-client': './src/packages/api-client/src',
'@geniuswebsrl/feature-attendance': './src/packages/feature-attendance/src',
'@geniuswebsrl/feature-leaves': './src/packages/feature-leaves/src',
'@geniuswebsrl/feature-nfc': './src/packages/feature-nfc/src',
'@geniuswebsrl/feature-sickness': './src/packages/feature-sickness/src',
'@geniuswebsrl/feature-geofencing': './src/packages/feature-geofencing/src',
},
},
],
]
: []),
],
};
Tre cose importanti qui.
1. react-native-dotenv ci permette di importare variabili d’ambiente con import { API_BASE_URL } from '@env'. Il file .env viene caricato anche da Babel stesso tramite require('dotenv').config() alla prima riga.
2. nativewind/babel nel preset abilita il compilatore di NativeWind.
3. L’alias condizionale module-resolver è il cuore della nostra architettura. Quando USE_LOCAL_PACKAGES=true nel .env, Babel risolve gli import @geniuswebsrl/* verso le cartelle locali in src/packages/. Quando è false, risolve dai package npm pubblicati su GitHub Packages.
Questo significa che possiamo sviluppare i package localmente con hot reload, e poi pubblicarli su npm per altri progetti senza cambiare una riga di codice. Il toggle è una variabile d’ambiente.
NativeWind: Tailwind CSS in React Native
NativeWind porta le classi di Tailwind CSS in React Native. La configurazione richiede tre file.
global.css
Un file CSS minimale alla radice del progetto:
@tailwind base;
@tailwind components;
@tailwind utilities;
Viene importato in App.tsx con import "./global.css". Metro lo processa tramite withNativeWind e lo trasforma in stili React Native.
tailwind.config.js
/** @type {import('tailwindcss').Config} */
const plugin = require('tailwindcss/plugin');
const { hairlineWidth } = require('nativewind/theme');
module.exports = {
content: [
"./App.tsx",
"./components/**/*.{js,jsx,ts,tsx}",
"./src/**/*.{js,jsx,ts,tsx}",
],
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
loginCard: '#4f332fd1',
loginButton: '#b22222',
loginButtonPressed: '#8a1a1a',
amber: '#F59E0B',
yellow: '#EAB308',
},
borderWidth: {
hairline: hairlineWidth(),
},
spacing: {
'15': '60px',
'25': '100px',
},
fontSize: {
'5xl': '50px',
'6xl': '60px',
'3xl': '32px',
},
},
},
plugins: [
plugin(({ addUtilities }) => {
addUtilities({
'.custom-elevate': {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 5,
},
});
}),
],
};
La proprietà content è critica: dice a Tailwind quali file scansionare per trovare le classi utilizzate. Se dimentichi un path, le classi in quei file non verranno compilate.
presets: [require("nativewind/preset")] abilita le utility specifiche di React Native, come hairlineWidth() che mappa correttamente il border width sottile su entrambe le piattaforme.
I colori custom (loginCard, loginButton, ecc.) sono esempi di quando hai bisogno di colori di brand che non sono nel palette default di Tailwind.
nativewind-env.d.ts
Per il supporto TypeScript, un file di dichiarazione:
/// <reference types="nativewind/types" />
Senza questo, TypeScript non riconosce le classi NativeWind nei file .tsx.
Il file Gemfile e Ruby per iOS
Ne abbiamo parlato nei prerequisiti, ma vale la pena approfondire. Il Gemfile vive alla radice del progetto e definisce le dipendenze Ruby per iOS.
source 'https://rubygems.org'
ruby ">= 2.6.10"
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.4'
Per installare le dipendenze:
bundle install
E da quel momento usi bundle exec pod install invece di pod install direttamente. Questo garantisce che tutti gli sviluppatori del team usino le stesse identiche versioni.
Il motivo per cui blocchiamo concurrent-ruby sotto 1.3.4 è che la 1.3.4 ha introdotto un cambiamento che rompe la risoluzione delle dipendenze di CocoaPods in alcuni scenari. Se hai mai visto un errore tipo Gem::ConflictError durante pod install, probabilmente è questo.
Podfile: CocoaPods e i fix per Firebase
Il Podfile è il file che più di ogni altro mi ha fatto perdere tempo. Ho passato due giorni a risolvere conflitti Firebase prima di arrivare a questa configurazione. Ecco il nostro, con i commenti alle parti non ovvie:
use_frameworks! :linkage => :static
use_modular_headers!
require Pod::Executable.execute_command('node', ['-p',
'require.resolve(
"react-native/scripts/react_native_pods.rb",
{paths: [process.argv[1]]},
)', __dir__]).strip
platform :ios, min_ios_version_supported
prepare_react_native_project!
target 'myGenius' do
config = use_native_modules!
pod 'FirebaseCoreInternal', :modular_headers => true
pod 'GoogleUtilities', :modular_headers => true
pod 'FirebaseAuth'
pod 'RecaptchaEnterprise', :modular_headers => true
use_react_native!(
:path => config[:reactNativePath],
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false
)
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if target.name == 'fmt'
config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++17'
end
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'COCOAPODS=1']
config.build_settings['OTHER_LDFLAGS'] ||= ['$(inherited)', '-ObjC']
config.build_settings['EXCLUDED_ARCHITECTURES'] = 'arm64'
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
config.build_settings['CODE_SIGNING_REQUIRED'] = 'NO'
end
end
end
end
Le parti che richiedono spiegazione:
use_frameworks! :linkage => :static — Linka i pod come framework statici invece che dinamici. Riduce i tempi di avvio e risolve conflitti con librerie che non supportano il linking dinamico.
use_modular_headers! — Abilita i modular headers per tutti i pod. Alcune librerie (Firebase in primis) li richiedono.
I pod Firebase espliciti — FirebaseCoreInternal e GoogleUtilities hanno bisogno di modular headers forzati. Senza, la build fallisce con errori di compilazione oscuri. RecaptchaEnterprise è una dipendenza di FirebaseAuth che ha lo stesso problema.
EXCLUDED_ARCHITECTURES = 'arm64' — Esclude l’architettura arm64 dai pod. Serve per compilare su simulatori Intel. Se hai un Mac con Apple Silicon non è strettamente necessario, ma tenerlo non crea problemi.
CODE_SIGNING_ALLOWED = 'NO' per i pod — I pod non devono essere firmati. Questo previene errori tipo “does not support provisioning profiles” quando compili per simulatori.
fmt con C++17 — La libreria fmt (dipendenza interna di alcune librerie C++) ha un bug con il default di Xcode. Forzare C++17 risolve l’errore di compilazione.
Dopo aver modificato il Podfile:
cd ios && bundle exec pod install
Android: build.gradle, SDK e keystore
La configurazione Android è divisa in due file build.gradle: uno di progetto e uno di app.
android/build.gradle (progetto)
buildscript {
ext {
buildToolsVersion = "35.0.0"
minSdkVersion = 26
compileSdkVersion = 35
targetSdkVersion = 35
ndkVersion = "27.1.12297006"
kotlinVersion = "2.0.21"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
classpath('com.google.gms:google-services:4.3.15')
}
}
subprojects {
project.configurations.all {
resolutionStrategy {
force 'com.google.android.gms:play-services-location:21.0.1'
}
}
}
apply plugin: "com.facebook.react.rootproject"
Il blocco resolutionStrategy forza una versione specifica di play-services-location. Questo previene conflitti quando diverse librerie richiedono versioni diverse della stessa dipendenza Google. Senza, la build può fallire con Duplicate class errors.
google-services:4.3.15 è il plugin di Firebase per Android. Lo abilitiamo anche nell’app build.gradle con apply plugin: 'com.google.gms.google-services'.
android/app/build.gradle (app)
Le parti rilevanti:
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
apply plugin: 'com.google.gms.google-services'
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
android {
namespace "it.mygenius.app"
defaultConfig {
applicationId "it.mygenius.app"
versionCode 43
versionName "2.8.1"
}
signingConfigs {
release {
if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
storeFile file(MYAPP_UPLOAD_STORE_FILE)
storePassword MYAPP_UPLOAD_STORE_PASSWORD
keyAlias MYAPP_UPLOAD_KEY_ALIAS
keyPassword MYAPP_UPLOAD_KEY_PASSWORD
}
}
}
buildTypes {
release {
signingConfig signingConfigs.release
shrinkResources false
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
Il namespace è it.mygenius.app — il reverse domain name che identifica l’app. L’applicationId deve coincidere.
Il signingConfigs.release legge le variabili di firma da gradle.properties (che non va committato). Le variabili MYAPP_UPLOAD_* contengono il path al keystore di produzione e le password. Se le proprietà non esistono (per esempio in un build di debug), il blocco viene saltato.
shrinkResources false e minifyEnabled false — abbiamo disabilitato ProGuard per ora. La ragione è che alcune librerie (in particolare react-native-nfc-manager e react-native-webview) richiedono regole ProGuard custom che non abbiamo ancora scritto. Meglio un APK più grande che una build che crasha in produzione.
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" — Collega i font di react-native-vector-icons automaticamente. Senza, le icone non verrebbero compilate nel bundle.
react-native.config.js e asset personalizzati
Questo file configura il CLI di React Native a livello di progetto:
module.exports = {
project: {
ios: {},
android: {},
},
assets: ['./src/assets/fonts'],
};
La proprietà assets dice al CLI dove cercare i font personalizzati. Quando esegui npx react-native-asset, i font in ./src/assets/fonts/ vengono copiati nei progetti nativi iOS e Android.
Nel nostro caso, usiamo il font Anton per i titoli. Senza questo file e senza il comando npx react-native-asset, il font non verrebbe riconosciuto.
tsconfig.json: TypeScript minimalista
React Native 0.78.1 viene con un preset TypeScript ufficiale. Il nostro tsconfig.json è una riga:
{
"extends": "@react-native/typescript-config/tsconfig.json"
}
Tutto qui. Il preset ufficiale configura strict, jsx, esModuleInterop, i path e tutto il resto. Se hai bisogno di override, aggiungi le chiavi necessarie:
{
"extends": "@react-native/typescript-config/tsconfig.json",
"compilerOptions": {
"strict": false,
"paths": {
"@/*": ["./src/*"]
}
}
}
Noi non abbiamo avuto bisogno di override. Meno config hai, meno cose possono rompersi in futuro.
Il entry point: index.js e App.tsx
index.js
/**
* @format
*/
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import messaging from '@react-native-firebase/messaging';
import { backgroundMessageHandler } from './src/utils/firebase';
import 'react-native-gesture-handler';
AppRegistry.registerComponent(appName, () => App);
Il commento @format dice a Prettier di formattare il file. Importiamo react-native-gesture-handler prima di tutto il resto — è un requisito di React Navigation per evitare warning e bug con i gesture.
L’import di messaging e backgroundMessageHandler è preparato per le notifiche push in background, anche se attualmente la riga messaging().setBackgroundMessageHandler(backgroundMessageHandler) è commentata. La lasceremo attiva quando integreremo completamente le push notifications.
app.json contiene il nome dell’app:
{
"name": "myGenius",
"displayName": "myGenius"
}
App.tsx
Il file App.tsx è il primo componente React che viene montato. Nel nostro caso gestisce tre stati prima di mostrare l’app vera e propria:
import React, { useState, useEffect } from 'react';
import { SafeAreaView, ActivityIndicator, Text, Image } from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import checkConnectionOnStart from './src/services/networkService';
import VersionModal from './src/components/modals/VersionModal';
import AppNavigator from './src/navigators/useAppNavigator';
import { globalStyles } from '@geniuswebsrl/mobile-ui';
import { AppProviders } from './src/app/AppProviders';
import "./global.css"
const App = () => {
const [isFirstScreen, setIsFirstScreen] = useState(true);
const [isOffline, setIsOffline] = useState(false);
const [mustBlock, setMustBlock] = useState(false);
const [updateUrl, setUpdateUrl] = useState(null);
useEffect(() => {
const init = async () => {
await checkConnectionOnStart(setIsOffline, setMustBlock, setUpdateUrl, setIsFirstScreen);
};
init();
}, []);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
const connected = state.isConnected && state.isInternetReachable !== false;
setIsOffline(!connected);
if (connected && !mustBlock) {
setIsFirstScreen(false);
}
});
return () => unsubscribe();
}, [mustBlock]);
if (mustBlock) {
return <VersionModal visible={true} updateUrl={updateUrl} />;
}
if (isFirstScreen) {
return (
<SafeAreaView style={globalStyles.splashContainer}>
<ActivityIndicator size="large" style={{ transform: [{ scale: 3 }] }} color="#cc66ff" />
</SafeAreaView>
);
}
if (isOffline) {
return (
<SafeAreaView style={globalStyles.errorContainer}>
<Text style={[globalStyles.errorText, globalStyles.antonRegular]}>CONNESSIONE ASSENTE</Text>
<Image source={require('./src/assets/no-connection.png')} style={globalStyles.noConnectionImage} />
</SafeAreaView>
);
}
return (
<AppProviders>
<AppNavigator />
</AppProviders>
)
};
export default App;
Il flusso è: splash screen → controllo connessione e versione → se offline mostra schermata offline → se forced update mostra VersionModal → altrimenti mostra l’app dentro AppProviders.
AppProviders è l’albero di provider che avvolge tutta l’app: ThemeProvider, PaperProvider, Redux Store, AuthProvider, ErrorModalProvider, AlertModalProvider. Ne parleremo in dettaglio in un articolo dedicato.
Package locali con alias Babel e .npmrc
myGenius usa un’architettura modulare con package “ciechi” — feature package che non conoscono HTTP, endpoint, o token. Vivono in src/packages/ e sono referenziati tramite alias Babel.
Il file .npmrc alla radice:
@geniuswebsrl:registry=https://npm.pkg.github.com
Dice a npm di risolvere i package con scope @geniuswebsrl dal registry di GitHub Packages invece di npm pubblico. Quando i package vengono pubblicati, vengono installati da qui.
Ma durante lo sviluppo, l’alias Babel nel babel.config.js (che abbiamo visto prima) risolve gli stessi import verso le cartelle locali. Il toggle è la variabile d’ambiente USE_LOCAL_PACKAGES.
Questo significa che questo codice:
import { NfcScreen } from '@geniuswebsrl/feature-nfc';
In sviluppo risolve a ./src/packages/feature-nfc/src, in produzione risolve al package npm pubblicato su GitHub Packages. Stesso codice, due sorgenti diversi, zero modifiche.
I package nel package.json sono dichiarati come dipendenze normali:
{
"dependencies": {
"@geniuswebsrl/api-client": "0.3.0",
"@geniuswebsrl/auth": "0.3.0",
"@geniuswebsrl/feature-attendance": "0.3.0",
"@geniuswebsrl/feature-leaves": "0.4.0",
"@geniuswebsrl/feature-nfc": "0.3.0",
"@geniuswebsrl/feature-sickness": "0.3.0",
"@geniuswebsrl/mobile-ui": "0.3.0"
}
}
Quando sviluppi localmente, l’alias Babel ha la precedenza e npm non viene contattato. Quando pubblichi, npm installa i package dal registry.
Primo avvio: iOS e Android
iOS
Dopo aver configurato tutto, il primo avvio iOS richiede:
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
Oppure, se hai un simulatore specifico:
npx react-native run-ios --simulator="iPhone 15 Pro"
Per compilare su dispositivo fisico, apri ios/myGenius.xcworkspace in Xcode, seleziona il tuo team di firma in Signing & Capabilities, e premi Run.
Android
Per Android, prima assicurati che un emulatore o dispositivo sia connesso:
adb devices
Poi:
npx react-native run-android
Se hai varianti di build personalizzate, puoi specificarle:
npx react-native run-android --variant=debug
Metro
In entrambi i casi, Metro deve essere in esecuzione. Se non parte automaticamente:
npx react-native start
Errori comuni e soluzioni
“Command pod install failed”
Hai dimenticato bundle exec. Usa sempre:
bundle exec pod install
non pod install direttamente. Se non hai ancora fatto bundle install, fallo prima.
“Cannot find module ‘@geniuswebsrl/mobile-ui’”
Se stai sviluppando localmente, assicurati che USE_LOCAL_PACKAGES=true sia nel tuo file .env. Se stai usando i package pubblicati, assicurati di aver fatto npm install e di essere autenticato su GitHub Packages (npm login --scope=@geniuswebsrl --registry=https://npm.pkg.github.com).
“Build failed: duplicate class” su Android
Conflitto di versioni Google. Aggiungi o modifica il blocco resolutionStrategy nel build.gradle di progetto per forzare la versione.
“FirebaseCoreInternal build error” su iOS
Manca pod 'FirebaseCoreInternal', :modular_headers => true nel Podfile. Aggiungilo e riesegui pod install.
“Unable to load script. Make sure you’re running Metro”
Metro non è in esecuzione. Apri un terminale separato e lancia npx react-native start. Se il problema persiste, prova npx react-native start --reset-cache.
“Error: ENOENT: no such file or directory, open ‘.env’”
Il file .env non esiste. Crea un file .env alla radice del progetto con le tue variabili. Al minimo:
API_BASE_URL=https://your-api-url.com
USE_LOCAL_PACKAGES=true
Domande frequenti
Bare workflow o Expo nel 2026?
Dipende. Se hai bisogno di accesso hardware profondo (NFC, biometria, Bluetooth), integrazioni native complesse, o vuoi controllo totale sulla build, bare workflow. Se vuoi velocità di setup, OTA updates semplici, e non ti serve accesso hardware avanzato, Expo è ancora una scelta valida.
Posso usare NativeWind senza il bare workflow?
Sì, NativeWind funziona anche con Expo. La configurazione è leggermente diversa (usa metro.config.js di Expo), ma il risultato è lo stesso.
Perché usi fetch invece di axios?
Perché fetch è nativo, non richiede dipendenze, e con un wrapper custom (createApiClient) gestiamo bearer token, timeout con AbortController, e errori tipizzati. axios aggiunge 13KB al bundle e un livello di astrazione che non ci serve. Ne parleremo in dettaglio in un articolo dedicato.
Come gestisci i certificati iOS?
Usiamo fastlane con match. Il Matchfile punta a un repository Git privato dove i certificati sono memorizzati crittografati. Ogni sviluppatore del team può recuperare i certificati con fastlane match development e fastlane match appstore.
Posso pubblicare i package su npm pubblico invece di GitHub Packages?
Sì. Cambia il .npmrc per puntare al registry npm pubblico e pubblica con npm publish --access public. Noi usiamo GitHub Packages perché i package sono privati e vogliamo che siano accessibili solo alla nostra organizzazione.
Conclusione
Configurare un progetto React Native bare workflow richiede tempo. Non è un npx create-expo-app e via. Ci ho messo tre giorni pieni per avere un setup che compilasse senza warning su entrambe le piattaforme, e altri due per sistemare i conflitti Firebase nel Podfile.
Ma il controllo che ottieni in cambio — su ogni dipendenza nativa, ogni versione, ogni flag di compilazione — ripaga ogni ora spesa. Quando qualcosa si rompe in CI/CD, sai esattamente dove guardare. Quando una libreria ha un bug, puoi patcharla senza aspettare che Expo rilasci un aggiornamento del suo SDK.
Abbiamo coperto tutto: dai prerequisiti (Node, Ruby, Android SDK, Xcode) alla configurazione di Metro e babel.config.js con alias condizionali, dal Podfile con i fix per Firebase ai file build.gradle per Android, dal sistema di package locali con toggle via environment variable al entry point con splash, offline detection e version check.
Nel prossimo articolo entriamo nel vivo dell’autenticazione: perché memorizziamo il token JWT in Keychain invece di AsyncStorage, e come react-native-keychain risolve il problema con una singola API su iOS e Android.
Se stai configurando il tuo primo progetto bare workflow e ti blocchi su qualcosa, controlla la sezione “Errori comuni” qui sopra. Se non trovi la risposta, scrivimi nei commenti — probabilmente ci sono passato anche io e ho ancora i log di errore salvati da qualche parte.




