From 41b0180b441aaa02251073ccb193359773bd8aab Mon Sep 17 00:00:00 2001 From: airkjw Date: Sat, 13 Jun 2026 12:10:03 +0900 Subject: [PATCH] feat(analytics): wire Firebase Analytics into app startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flutterfire configure registered the iOS/Android apps under project block-seasons and generated firebase_options.dart + native config. main() now initializes Firebase and routes analytics through FirebaseAnalyticsBackend in release builds (console logger in debug, so dev traffic never pollutes GA4). Firebase init is guarded — failure falls back to the debug logger rather than blocking startup. firebase.json keeps the existing Hosting config and gains the FlutterFire platform section. Client config files are committed (they ship in the binary; Firebase security is enforced by rules, not config secrecy). flutter analyze clean, all 161 tests green. Co-Authored-By: Claude Fable 5 --- android/app/build.gradle.kts | 3 ++ android/app/google-services.json | 29 ++++++++++++ android/settings.gradle.kts | 3 ++ firebase.json | 11 +---- ios/Runner.xcodeproj/project.pbxproj | 4 ++ ios/Runner/GoogleService-Info.plist | 30 ++++++++++++ lib/firebase_options.dart | 68 ++++++++++++++++++++++++++++ lib/main.dart | 22 +++++++++ 8 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 ios/Runner/GoogleService-Info.plist create mode 100644 lib/firebase_options.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 258055c..5b8a1c1 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,5 +1,8 @@ plugins { id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..15ebe30 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "190209969950", + "project_id": "block-seasons", + "storage_bucket": "block-seasons.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:190209969950:android:e08dc30877b1821b44c30f", + "android_client_info": { + "package_name": "com.airkjw.blockseasons" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCd_3Tw5IxlO6ysp3XMQ9tBsOM1yMYl2MU" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index fb605bc..e2e6ffb 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -20,6 +20,9 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.9.1" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.4.4") apply false + // END: FlutterFire Configuration id("org.jetbrains.kotlin.android") version "2.1.0" apply false } diff --git a/firebase.json b/firebase.json index 74ab3aa..1a54372 100644 --- a/firebase.json +++ b/firebase.json @@ -1,10 +1 @@ -{ - "hosting": { - "public": "deploy", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ] - } -} +{"hosting":{"public":"deploy","ignore":["firebase.json","**/.*","**/node_modules/**"]},"flutter":{"platforms":{"android":{"default":{"projectId":"block-seasons","appId":"1:190209969950:android:e08dc30877b1821b44c30f","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"block-seasons","appId":"1:190209969950:ios:f9d4578ec86f92c844c30f","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"block-seasons","configurations":{"android":"1:190209969950:android:e08dc30877b1821b44c30f","ios":"1:190209969950:ios:f9d4578ec86f92c844c30f"}}}}}} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 02018ee..a49888a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; A1624C49AABB61D3BB6EBA00 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F021B835BC4E346AE82B4C9 /* Pods_RunnerTests.framework */; }; D444497F007A61A9102D174D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92F5ACA56D636C056F52DDE6 /* Pods_Runner.framework */; }; + E746073DDE80B82D8D3C9659 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 43C03408D7DF6E3F6C4EC9C7 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -47,6 +48,7 @@ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 43C03408D7DF6E3F6C4EC9C7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 55914DA7E8E89CB02E73C3F5 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 6BD9A45428DD4E519FC38754 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; @@ -129,6 +131,7 @@ 331C8082294A63A400263BE5 /* RunnerTests */, 2EECBA43D42E2853F949CCFC /* Pods */, 9CFCC4FE458D4EC11DAF9E88 /* Frameworks */, + 43C03408D7DF6E3F6C4EC9C7 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -264,6 +267,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + E746073DDE80B82D8D3C9659 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..fe9cd86 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyAIl2LM2fDrr7OH70K7uJnAwKwXMzPCBMI + GCM_SENDER_ID + 190209969950 + PLIST_VERSION + 1 + BUNDLE_ID + com.airkjw.blockseasons + PROJECT_ID + block-seasons + STORAGE_BUCKET + block-seasons.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:190209969950:ios:f9d4578ec86f92c844c30f + + \ No newline at end of file diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..48f1cb0 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,68 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCd_3Tw5IxlO6ysp3XMQ9tBsOM1yMYl2MU', + appId: '1:190209969950:android:e08dc30877b1821b44c30f', + messagingSenderId: '190209969950', + projectId: 'block-seasons', + storageBucket: 'block-seasons.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAIl2LM2fDrr7OH70K7uJnAwKwXMzPCBMI', + appId: '1:190209969950:ios:f9d4578ec86f92c844c30f', + messagingSenderId: '190209969950', + projectId: 'block-seasons', + storageBucket: 'block-seasons.firebasestorage.app', + iosBundleId: 'com.airkjw.blockseasons', + ); +} diff --git a/lib/main.dart b/lib/main.dart index d4e69f9..5c1dc86 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; @@ -8,6 +10,9 @@ import 'app.dart'; import 'data/content_repository.dart'; import 'data/remote/content_downloader.dart'; import 'data/save_repository.dart'; +import 'firebase_options.dart'; +import 'services/analytics_service.dart'; +import 'services/firebase_analytics_backend.dart'; import 'state/providers.dart'; /// Remote content origin. Swap per environment with @@ -22,6 +27,22 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); final saveRepository = await SaveRepository.open(); + // Analytics: real GA4 traffic flows only from release builds so development + // never pollutes production. If Firebase init fails (e.g. missing native + // config), fall back to the console logger rather than blocking startup. + AnalyticsService analytics; + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + analytics = AnalyticsService( + kReleaseMode ? FirebaseAnalyticsBackend() : DebugAnalyticsBackend(), + ); + } catch (e) { + debugPrint('Firebase init failed, analytics disabled: $e'); + analytics = AnalyticsService(DebugAnalyticsBackend()); + } + ContentRepository contentRepository; try { final support = await getApplicationSupportDirectory(); @@ -42,6 +63,7 @@ Future main() async { overrides: [ saveRepositoryProvider.overrideWithValue(saveRepository), contentRepositoryProvider.overrideWithValue(contentRepository), + analyticsProvider.overrideWithValue(analytics), ], child: const BlockSeasonsApp(), ));