Files
halobestie-clone/client_app/lib/main.dart
ramadhan sjamsani 4ada7c991a Phase 4 Stage 0: design system foundation (client_app)
- HaloTokens, HaloSpacing, HaloRadius, HaloMotion, HaloShadows (warm palette;
  calm/playful stubbed for phase 5).
- Bundled Bricolage Grotesque, Poppins, JetBrains Mono (~1.2 MB total, OFL).
- haloThemeData() wired into MaterialApp.router with Figma-aligned text
  scale, pill ElevatedButton, 64px input height, 24px-corner BottomSheet,
  dark pill SnackBar.
- Halo* widget primitives: Button, Orb, StepDots, BottomSheet, Popup,
  Snackbar, Chip.
- Dev-only /_theme_preview route gated by --dart-define=THEME_PREVIEW=true
  for visual reference during stages 2-8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:56:00 +08:00

92 lines
2.8 KiB
Dart

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/api/api_client_provider.dart';
import 'core/auth/auth_notifier.dart';
import 'core/chat/active_session_notifier.dart';
import 'core/chat/chat_notifier.dart';
import 'core/notifications/notification_service.dart';
import 'core/theme/halo_theme.dart';
import 'firebase_options.dart';
import 'router.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
final messaging = FirebaseMessaging.instance;
await messaging.requestPermission();
runApp(const ProviderScope(child: App()));
}
class App extends ConsumerStatefulWidget {
const App({super.key});
@override
ConsumerState<App> createState() => _AppState();
}
class _AppState extends ConsumerState<App> {
bool _fcmRegistered = false;
void _registerFcmToken() {
if (_fcmRegistered) return;
_fcmRegistered = true;
Future(() async {
try {
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
await ref.read(apiClientProvider).post('/api/shared/device-token', data: {'token': token});
}
} catch (_) {
_fcmRegistered = false;
}
});
}
@override
Widget build(BuildContext context) {
// FCM registration on auth.
ref.listen(authProvider, (prev, next) {
final data = next.valueOrNull;
if (data is AuthAuthenticatedData || data is AuthAnonymousData) {
_registerFcmToken();
} else {
// Logged out (or initial) — ensure the chat WS is closed.
ref.read(chatProvider.notifier).disconnect();
}
});
// Global chat WebSocket lifecycle: connect whenever the user has an
// active session, regardless of which screen is mounted. The chat screen
// only joins this connection — it doesn't own it. FCM remains the
// background-only fallback.
ref.listen(activeSessionProvider, (prev, next) {
final snapshot = next.valueOrNull;
final notifier = ref.read(chatProvider.notifier);
if (snapshot == null || !snapshot.hasSession) {
if (notifier.connectedSessionId != null) {
notifier.disconnect();
}
return;
}
final sessionId = snapshot.sessionId;
if (sessionId != null && notifier.connectedSessionId != sessionId) {
notifier.connectIfNotConnected(sessionId);
}
});
final router = ref.watch(routerProvider);
NotificationService.initialize(router);
return MaterialApp.router(
title: 'Halo Bestie',
theme: haloThemeData(),
routerConfig: router,
);
}
}