quitar AdMob, Firebase Analytics y ATT de la app

"Portfolio Journal sin AdMob ni Google: es la única forma de que 'sin
rastreo' sea verdad, y el ingreso por anuncios es despreciable" — la ficha
de App Store prometía "sin analíticas, sin rastreo, sin venta de datos"
mientras la app enlazaba GoogleMobileAds y FirebaseAnalytics, pedía permiso
de rastreo al arrancar y mandaba el importe del saldo a GA4 en el evento
snapshot_added.

SDKs fuera del proyecto (project.pbxproj): paquetes firebase-ios-sdk y
swift-package-manager-google-mobile-ads, productos FirebaseCore,
FirebaseAnalytics, GoogleMobileAds y FirebaseCrashlytics, más la fase de
build "Upload dSYMs to Crashlytics". Package.resolved se queda sin nada que
resolver. Con la fase de script fuera, ENABLE_USER_SCRIPT_SANDBOXING vuelve
a YES en el target app (estaba en NO solo por Crashlytics).

Código borrado:
- Services/AdMobService.swift entero — con él se van el flujo UMP,
  BannerAdView, BannerAdCoordinator y la llamada a
  ATTrackingManager.requestTrackingAuthorization().
- Services/FirebaseService.swift entero y sus 40 llamadas. Se borra en vez
  de dejarse como capa vacía: una clase llamada FirebaseService en una app
  que presume de no llevar Firebase es exactamente la clase de detalle que
  vuelve a colarse en una auditoría dentro de un año.
- El banner y su safeAreaInset en ContentView (bannerInsetView): las cinco
  pestañas ya no reservan 50 pt al pie.
- La entrada "Manage Ad Consent" de Ajustes y el @EnvironmentObject
  adMobService de SettingsView, ContentView y PortfolioJournalApp.
- AppConstants: bloque AdMob, bannerAdHeight, adConsentObtained,
  Features.enableAnalytics.
- SettingsViewModel.toggleAnalytics y analyticsEnabled — el flag no estaba
  conectado a nada ni tenía control en la interfaz. El atributo
  AppSettings.enableAnalytics se queda en CoreData con un comentario: no
  vale la pena una migración y un deploy de esquema CloudKit por borrarlo.
- Premium ya no promete "sin anuncios": fuera PremiumFeature.noAds, la
  entrada de IAPService.premiumFeatures y paywallBenefits, y las claves
  feature_no_ads / paywall_benefit_noads de los 7 idiomas. No se toca ni el
  precio ni el producto.

Info.plist: fuera NSUserTrackingUsageDescription, GADApplicationIdentifier,
GADDelayAppMeasurementInit y los 65 SKAdNetworkItems. Borrados también
GoogleService-Info.plist y los scripts Scripts/analyze_ga4.py y
Scripts/analyze_crashlytics.py, que ya no tienen de dónde leer.

Closes #50

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
This commit is contained in:
alexandrev-tibco
2026-09-18 16:09:04 +02:00
parent 319ab9589e
commit 7d2f605c16
39 changed files with 23 additions and 1503 deletions
@@ -405,8 +405,6 @@ class ChartsViewModel: ObservableObject {
category: selectedCategory,
timeRange: selectedTimeRange
)
FirebaseService.shared.logScreenView(screenName: "Charts")
}
/// Selection is NEVER blocked premium charts select normally and the chart
@@ -423,10 +421,6 @@ class ChartsViewModel: ObservableObject {
// observer: makes selection deterministic and lets re-tapping a tile
// act as a retry if a previous update was dropped.
updateChartData(chartType: chartType, category: selectedCategory, timeRange: selectedTimeRange)
FirebaseService.shared.logChartViewed(
chartType: chartType.rawValue,
isPremium: chartType.isPremium
)
}
func availableTimeRanges(for chartType: ChartType) -> [TimeRange] {
@@ -11,7 +11,6 @@ class SettingsViewModel: ObservableObject {
@Published var isFamilyShared = false
@Published var notificationsEnabled = false
@Published var defaultNotificationTime = Date()
@Published var analyticsEnabled = true
@Published var currencyCode = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency
@Published var inputMode: InputMode = .simple
@@ -93,7 +92,6 @@ class SettingsViewModel: ObservableObject {
let settings = AppSettings.getOrCreate(in: context)
defaultNotificationTime = settings.defaultNotificationTime ?? Date()
analyticsEnabled = settings.enableAnalytics
currencyCode = settings.currency
inputMode = InputMode(rawValue: settings.inputMode) ?? .simple
backupRetentionCount = loadBackupRetention()
@@ -107,8 +105,6 @@ class SettingsViewModel: ObservableObject {
// Load statistics directly from database to avoid async race conditions
loadStatistics()
FirebaseService.shared.logScreenView(screenName: "Settings")
}
private func loadStatistics() {
@@ -131,7 +127,6 @@ class SettingsViewModel: ObservableObject {
func upgradeToPremium() {
showingPaywall = true
FirebaseService.shared.logPaywallShown(trigger: "settings_upgrade")
}
func restorePurchases() async {
@@ -141,10 +136,8 @@ class SettingsViewModel: ObservableObject {
if isPremium {
successMessage = "Purchases restored successfully!"
FirebaseService.shared.logRestorePurchases(success: true)
} else {
errorMessage = "No purchases to restore"
FirebaseService.shared.logRestorePurchases(success: false)
}
}
@@ -191,7 +184,6 @@ class SettingsViewModel: ObservableObject {
func exportData(format: ExportService.ExportFormat) {
guard freemiumValidator.canExport() else {
showingPaywall = true
FirebaseService.shared.logPaywallShown(trigger: "export")
return
}
@@ -242,13 +234,11 @@ class SettingsViewModel: ObservableObject {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.shareItem = ShareItem(url: tempURL)
}
FirebaseService.shared.logExportAttempt(format: format.rawValue, success: true)
}
} catch {
await MainActor.run {
isExporting = false
errorMessage = "Export failed: \(error.localizedDescription)"
FirebaseService.shared.logExportAttempt(format: format.rawValue, success: false)
}
}
}
@@ -272,19 +262,6 @@ class SettingsViewModel: ObservableObject {
freemiumValidator.canExport()
}
// MARK: - Analytics
func toggleAnalytics(_ enabled: Bool) {
analyticsEnabled = enabled
let context = CoreDataStack.shared.viewContext
let settings = AppSettings.getOrCreate(in: context)
settings.enableAnalytics = enabled
CoreDataStack.shared.save()
// Note: In production, you'd also update Firebase Analytics consent
}
// MARK: - Currency
func updateCurrency(_ code: String) {
@@ -555,7 +532,6 @@ class SettingsViewModel: ObservableObject {
guard isPremium else {
backupsEnabled = false
UserDefaults.standard.set(false, forKey: backupsEnabledKey)
FirebaseService.shared.logPaywallShown(trigger: "backups")
showingPaywall = true
return
}
@@ -87,8 +87,6 @@ class SourceDetailViewModel: ObservableObject {
isLoading = true
refreshData()
isLoading = false
FirebaseService.shared.logScreenView(screenName: "SourceDetail")
}
func refreshData() {
@@ -167,9 +165,6 @@ class SourceDetailViewModel: ObservableObject {
// Reschedule notification
NotificationService.shared.scheduleReminder(for: source)
// Log analytics
FirebaseService.shared.logSnapshotAdded(sourceName: source.name, value: value)
showingAddSnapshot = false
refreshData()
}
@@ -216,9 +211,6 @@ class SourceDetailViewModel: ObservableObject {
// Cancel any pending notifications for this source
NotificationService.shared.cancelReminder(for: source)
// Log analytics before deletion
FirebaseService.shared.logSourceDeleted(categoryName: source.category?.name ?? "Uncategorized")
// Delete the source using the existing repository
sourceRepository.deleteSource(source)
}
@@ -226,14 +218,9 @@ class SourceDetailViewModel: ObservableObject {
// MARK: - Predictions
func showPredictions() {
if freemiumValidator.canViewPredictions() {
// Already loaded, just navigate
FirebaseService.shared.logPredictionViewed(
algorithm: predictionResult?.algorithm.rawValue ?? "unknown"
)
} else {
// Predictions are already loaded; free users get the paywall instead.
if !freemiumValidator.canViewPredictions() {
showingPaywall = true
FirebaseService.shared.logPaywallShown(trigger: "predictions")
}
}
@@ -76,8 +76,6 @@ class SourceListViewModel: ObservableObject {
sourceRepository.fetchSources()
categoryRepository.fetchCategories()
isLoading = false
FirebaseService.shared.logScreenView(screenName: "SourceList")
}
private func filterAndSortSources(_ allSources: [InvestmentSource]) {
@@ -115,7 +113,6 @@ class SourceListViewModel: ObservableObject {
showingAddSource = true
} else {
showingPaywall = true
FirebaseService.shared.logPaywallShown(trigger: "source_limit")
}
}
@@ -137,26 +134,15 @@ class SourceListViewModel: ObservableObject {
// Schedule notification
NotificationService.shared.scheduleReminder(for: source)
// Log analytics
FirebaseService.shared.logSourceAdded(
categoryName: category.name,
sourceCount: sourceRepository.sourceCount
)
showingAddSource = false
}
func deleteSource(_ source: InvestmentSource) {
let categoryName = source.category?.name ?? "Unknown"
// Cancel notifications
NotificationService.shared.cancelReminder(for: source)
// Delete source
sourceRepository.deleteSource(source)
// Log analytics
FirebaseService.shared.logSourceDeleted(categoryName: categoryName)
}
func deleteSource(at offsets: IndexSet) {