Docs/Firebase_Notifications_Guide.md
This guide explains how Firebase Push Notifications are integrated into your Bagisto Flutter app, with support for login, register, and logout flows.
┌─────────────────────────────────────────────────────────┐
│ Firebase Service │
│ - Initializes Firebase Core │
│ - Manages SDK configuration │
└────────────────────┬────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────┐
│ FCM Service │
│ - Handles push notifications │
│ - Manages foreground/background/terminated states │
│ - Displays local notifications │
└────────────────────┬────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────┐
│ Device Token Service │
│ - Stores/retrieves FCM tokens locally │
│ - Manages token lifecycle │
│ - Checks token staleness │
└────────────────────┬────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────┐
│ Auth Repository & Bloc │
│ - Sends device token on login/register │
│ - Clears device token on logout │
│ - Manages auth tokens │
└─────────────────────────────────────────────────────────┘
lib/
├── core/
│ └── notifications/
│ ├── firebase_service.dart # Firebase initialization
│ ├── firebase_options.dart # Platform-specific config
│ ├── device_token_service.dart # Token management
│ └── fcm_service.dart # FCM handler
├── features/
│ └── auth/
│ ├── data/
│ │ └── repository/
│ │ └── auth_repository.dart # Updated with device token
│ └── presentation/
│ └── bloc/
│ └── auth_bloc.dart # Updated event/state
└── main.dart # Firebase initialization
main() before app runsFirebaseService.initialize()await FirebaseService.initialize();
await FCMService().initialize(
onForegroundMessage: _handleForegroundNotification,
onBackgroundMessage: _handleBackgroundNotification,
onMessageOpenedApp: _handleMessageOpenedApp,
);
// Save token
await DeviceTokenService.saveDeviceToken(token);
// Get token
final token = await DeviceTokenService.getDeviceToken();
// Clear on logout
await DeviceTokenService.clearDeviceToken();
login() - sends deviceTokenregister() - sends deviceTokenlogout() - clears device tokenupdateDeviceToken() - updates token if refreshed// Login with device token
await repository.login(
email: email,
password: password,
deviceToken: deviceToken, // Automatically retrieved if not provided
);
AuthLoginRequested - accepts optional deviceTokenAuthRegisterRequested - accepts optional deviceTokencontext.read<AuthBloc>().add(
AuthLoginRequested(
email: email,
password: password,
deviceToken: fcmToken,
),
);
The login mutation now includes deviceToken parameter:
mutation loginCustomer($input: createCustomerLoginInput!, $deviceToken: String) {
createCustomerLogin(input: $input) {
customerLogin {
id
apiToken
token
message
success
}
}
}
The register mutation now includes deviceToken parameter:
mutation registerCustomer($input: createCustomerInput!, $deviceToken: String) {
createCustomer(input: $input) {
customer {
id
firstName
lastName
email
phone
status
apiToken
customerGroupId
subscribedToNewsLetter
isVerified
isSuspended
token
rememberToken
name
}
}
}
For refreshing token on server:
mutation updateDeviceToken($deviceToken: String!) {
updateDeviceToken(input: { deviceToken: $deviceToken }) {
success
message
}
}
When the app is in focus and receives a notification:
onForegroundMessage() is invokedFuture<void> _handleForegroundNotification(RemoteMessage message) async {
debugPrint('📬 Foreground notification');
// TODO: Navigate based on message data
}
When the app is in background:
onMessageOpenedApp is calledFuture<void> _handleMessageOpenedApp(RemoteMessage message) async {
debugPrint('📲 App opened from notification');
// TODO: Navigate based on message data
}
When the app is completely terminated:
getInitialMessage() retrieves the messagefinal initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
// Handle the message
}
User → Login Page → AuthBloc
↓
AuthLoginRequested(email, password)
↓
AuthRepository.login()
↓
Get deviceToken from DeviceTokenService
↓
Send to server with GraphQL mutation
↓
Server stores device token
↓
AuthAuthenticated state with deviceToken
User → Register Page → AuthBloc
↓
AuthRegisterRequested(...)
↓
AuthRepository.register()
↓
Get deviceToken from DeviceTokenService
↓
Send to server with GraphQL mutation
↓
Server stores device token
↓
AuthAuthenticated state with deviceToken
User → Logout → AuthBloc
↓
AuthLogoutRequested
↓
AuthRepository.logout()
↓
Clear device token from DeviceTokenService
↓
Clear local auth storage
↓
AuthUnauthenticated state
FCM → Token Refresh
↓
FCMService._handleTokenRefresh()
↓
Save new token to DeviceTokenService
↓
If user is authenticated:
AuthRepository.updateDeviceToken(newToken)
↓
Server updates device token
// In your login page
void _handleLogin() {
final email = _emailController.text;
final password = _passwordController.text;
context.read<AuthBloc>().add(
AuthLoginRequested(
email: email,
password: password,
// deviceToken is optional - automatically retrieved if not provided
),
);
}
// Listen to auth state
BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthAuthenticated) {
// User logged in, device token is sent to server
// Notifications can now be received
Navigator.of(context).pushReplacementNamed('/home');
} else if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
child: // ... your UI
)
// In main.dart - update the callback
Future<void> _handleMessageOpenedApp(RemoteMessage message) async {
debugPrint('📲 App opened from notification');
final data = message.data;
// Navigate based on notification type
if (data.containsKey('type')) {
switch (data['type']) {
case 'order':
// Navigate to order details
break;
case 'promotion':
// Navigate to promotion
break;
}
}
}
// Subscribe to promotions topic
await FCMService().subscribeToTopic('promotions');
// This allows you to send notifications to all users subscribed to 'promotions'
// from your server/Firebase console
# In Flutter app logs, look for:
# 🎫 Device token obtained: ...
// Add to main.dart for debugging
FirebaseMessaging.onMessage.listen((message) {
debugPrint('Foreground: ${message.notification?.title}');
});
FirebaseMessaging.onBackgroundMessage((message) {
debugPrint('Background: ${message.notification?.title}');
});
Symptoms: 🎫 Device token obtained: null in logs
Solutions:
Symptoms: No notifications appear despite being sent
Solutions:
Symptoms: Old tokens on server after app restart
Solutions:
updateDeviceToken() to syncSymptoms: Device token not sent during login
Solutions:
DeviceTokenService.getDeviceToken() returns non-nullstatic Future<void> initialize()
Future<void> initialize({
NotificationCallback? onForegroundMessage,
NotificationCallback? onBackgroundMessage,
NotificationCallback? onMessageOpenedApp,
})
Future<String?> getDeviceToken()
Future<String?> refreshDeviceToken()
Future<void> setNotificationsEnabled(bool enabled)
Future<void> subscribeToTopic(String topic)
Future<void> unsubscribeFromTopic(String topic)
static Future<void> saveDeviceToken(String token)
static Future<String?> getDeviceToken()
static Future<void> clearDeviceToken()
static Future<DateTime?> getTokenGeneratedAt()
static Future<bool> isTokenStale()
Future<CustomerLogin> login({
required String email,
required String password,
String? deviceToken,
})
Future<Customer> register({
required String firstName,
required String lastName,
required String email,
required String password,
required String confirmPassword,
String? deviceToken,
})
Future<bool> logout()
Future<bool> updateDeviceToken({required String deviceToken})