Files
BlockSeasons/lib/main.dart
T
airkjw fa2784519b fix(boosters): address final-review findings
- daily claim: record the claim before granting boosters, so a crash
  mid-claim forfeits at most one reward instead of allowing a re-claim
  (booster farming) on next launch.
- game screen: disarm the booster target synchronously before awaiting,
  so a rapid second board tap can't double-fire a use or stack a dialog.
- new players: seed one of each booster once (idempotent persisted flag),
  fulfilling the spec's starting inventory. Wired in main().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:36:24 +09:00

94 lines
3.6 KiB
Dart

import 'dart:io';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.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';
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
/// --dart-define=CONTENT_BASE_URL=...; the default points at the production
/// Firebase Hosting site (owner setup pending).
const contentBaseUrl = String.fromEnvironment(
'CONTENT_BASE_URL',
defaultValue: 'https://block-seasons.web.app/content',
);
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final saveRepository = await SaveRepository.open();
// New players start with one of each booster (idempotent after the first run).
await saveRepository.seedInitialBoostersIfNeeded();
// 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,
);
// Keep development traffic out of GA4 entirely — this also silences the
// native SDK's automatic events (session_start, screen_view), not just
// our custom ones, so only release builds ever reach production analytics.
await FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(kReleaseMode);
analytics = AnalyticsService(
kReleaseMode ? FirebaseAnalyticsBackend() : DebugAnalyticsBackend(),
);
// Crashlytics: collect from release builds only (so debug keeps its red
// error screens and console traces, and development crashes never reach
// the production dashboard). In release, route both Flutter framework
// errors and uncaught async/platform errors to Crashlytics.
await FirebaseCrashlytics.instance
.setCrashlyticsCollectionEnabled(kReleaseMode);
if (kReleaseMode) {
FlutterError.onError =
FirebaseCrashlytics.instance.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
}
} catch (e) {
debugPrint('Firebase init failed, analytics disabled: $e');
analytics = AnalyticsService(DebugAnalyticsBackend());
}
ContentRepository contentRepository;
try {
final support = await getApplicationSupportDirectory();
final cacheDir = Directory('${support.path}/content');
contentRepository = ContentRepository(
cacheDir: cacheDir,
downloader: ContentDownloader(
baseUrl: contentBaseUrl,
cacheDir: cacheDir,
),
);
} catch (e) {
debugPrint('content cache unavailable, bundled only: $e');
contentRepository = ContentRepository();
}
runApp(ProviderScope(
overrides: [
saveRepositoryProvider.overrideWithValue(saveRepository),
contentRepositoryProvider.overrideWithValue(contentRepository),
analyticsProvider.overrideWithValue(analytics),
],
child: const BlockSeasonsApp(),
));
}