Ниже приведены мои службы уведомлений и код заставки
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
Function(String actionId, Map? payload)? onActionTapped;
isolates)
Future initialize({
bool forceReinit = false,
DidReceiveBackgroundNotificationResponseCallback?
onDidReceiveBackgroundNotificationResponse,
}) async {
if (Platform.isIOS) {
// 1. Invitation category - Accept/Reject buttons
iosCategories.add(
DarwinNotificationCategory(
'invitation_category',
actions: [
DarwinNotificationAction.plain(
'accept_invitation',
AppStrings.txtAccept,
options: {DarwinNotificationActionOption.foreground},
),
DarwinNotificationAction.plain(
'reject_invitation',
AppStrings.txtReject,
options: {DarwinNotificationActionOption.destructive},
),
],
options: {DarwinNotificationCategoryOption.hiddenPreviewShowTitle},
),
);
}
// iOS initialization settings with categories
final DarwinInitializationSettings iosSettings =
DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
notificationCategories: iosCategories,
);
final InitializationSettings initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
// Initialize the plugin
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: _onNotificationTapped,
onDidReceiveBackgroundNotificationResponse:
onDidReceiveBackgroundNotificationResponse,
);
// Request permissions
if (Platform.isAndroid) {
await _requestAndroidPermissions();
} else if (Platform.isIOS) {
await _requestIOSPermissions();
}
// Create notification channels for Android
await _createNotificationChannels();
_isInitialized = true;
debugPrint('
// Verify initialization worked
try {
if (Platform.isAndroid) {
final androidImplementation =
_localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidImplementation != null) {
debugPrint('
}
} else if (Platform.isIOS) {
final iosImplementation =
_localNotifications
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>();
if (iosImplementation != null) {
debugPrint('
}
}
} catch (e) {
debugPrint('
}
}
/// Request Android notification permissions
Future _requestAndroidPermissions() async {
if (Platform.isAndroid) {
final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
_localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidImplementation != null) {
final bool? granted =
await androidImplementation.requestNotificationsPermission();
debugPrint('Android notification permission granted: $granted');
}
}
}
final bool? granted = await iosImplementation.requestPermissions(
alert: true,
badge: true,
sound: true,
);
debugPrint('iOS notification permission granted: $granted');
}
Future _createNotificationChannels() async {
if (Platform.isAndroid) {
final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
_localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidImplementation != null) {
// Invitation channel
await androidImplementation.createNotificationChannel(
const AndroidNotificationChannel(
'invitation_channel',
AppStrings.txtInvitationNotifications,
description: AppStrings.txtNotificationsForCommitteeInvitations,
importance: Importance.high,
playSound: true,
enableVibration: true,
),
);
// Deposit reminder channel
await androidImplementation.createNotificationChannel(
const AndroidNotificationChannel(
'deposit_reminder_channel',
AppStrings.txtDepositReminders,
description: AppStrings.txtRemindersToDepositMoney,
importance: Importance.high,
playSound: true,
enableVibration: true,
),
);
// Host alerts channel
await androidImplementation.createNotificationChannel(
const AndroidNotificationChannel(
'host_alerts_channel',
AppStrings.txtHostAlerts,
description: AppStrings.txtNotificationsForCommitteeHosts,
importance: Importance.high,
playSound: true,
enableVibration: true,
),
);
}
}
/// Handle notification tap and action button taps
void _onNotificationTapped(NotificationResponse response) {
debugPrint('Notification tapped: ${response.id}');
debugPrint('Action ID: ${response.actionId}');
debugPrint('Payload: ${response.payload}');
if (response.actionId != null) {
// Action button was tapped
Map? payload;
if (response.payload != null) {
try {
// Try to parse payload as JSON string first
if (response.payload is String) {
final parsed =
jsonDecode(response.payload as String) as Map;
payload = parsed;
} else {
payload = {'data': response.payload};
}
} catch (e) {
// If parsing fails, wrap it in a data object
debugPrint('Error parsing payload: $e');
payload = {'data': response.payload};
}
}
payload ??= {};
payload['notification_id'] = response.id;
onActionTapped?.call(response.actionId!, payload);
} else {
// Notification body was tapped
Map? payload;
if (response.payload != null) {
try {
// Try to parse payload as JSON string
if (response.payload is String) {
final parsed =
jsonDecode(response.payload as String) as Map;
payload = parsed;
} else {
payload = {'id': response.id};
}
} catch (e) {
payload = {'id': response.id};
}
} else {
payload = {'id': response.id};
}
payload['notification_id'] = response.id;
onActionTapped?.call('notification_tapped', payload);
}
}
/// Process the notification/action that launched the app from terminated state.
Future handleAppLaunchNotificationAction() async {
final NotificationAppLaunchDetails? launchDetails =
await _localNotifications.getNotificationAppLaunchDetails();
if (launchDetails == null || !launchDetails.didNotificationLaunchApp) {
return;
}
final NotificationResponse? response = launchDetails.notificationResponse;
if (response != null) {
_onNotificationTapped(response);
}
}
/// Show a local notification with action buttons (for testing)
Future showInvitationNotification({
required String title,
required String body,
required String committeeId,
required String committeeTitle,
String? invitationId,
}) async {
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'invitation_channel',
AppStrings.txtInvitationNotifications,
channelDescription: AppStrings.txtNotificationsForCommitteeInvitations,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
// Define action buttons
actions: [
AndroidNotificationAction(
'accept_invitation',
AppStrings.txtAccept,
titleColor: Color(0xFF2196F3), // Blue color
showsUserInterface: true,
),
AndroidNotificationAction(
'reject_invitation',
AppStrings.txtReject,
titleColor: Color(0xFF757575), // Grey color
showsUserInterface: true,
),
],
);
const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
categoryIdentifier: 'invitation_category',
presentAlert: true,
presentBanner: true,
presentBadge: true,
presentList: true,
presentSound: true,
);
const NotificationDetails notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
// Create payload with committee information and invitation ID
final String payload =
invitationId != null && invitationId.isNotEmpty
? '{"committee_id": "$committeeId", "committee_title": "$committeeTitle", "type": "invitation", "typeId": "$invitationId"}'
: '{"committee_id": "$committeeId", "committee_title": "$committeeTitle", "type": "invitation"}';
final int notificationId =
int.tryParse(invitationId ?? '') ??
DateTime.now().millisecondsSinceEpoch.remainder(100000);
await _localNotifications.show(
notificationId,
title,
body,
notificationDetails,
payload: payload,
);
}
/// Show chat message notification with Reply button
Future showChatNotification({
required String title,
required String body,
required String committeeId,
String? senderId,
String? senderName,
}) async {
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'chat_channel',
AppStrings.txtChatNotifications,
channelDescription: AppStrings.txtNotificationsForChatMessages,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
actions: [
AndroidNotificationAction(
'reply',
AppStrings.txtReply,
titleColor: Color(0xFF2196F3),
showsUserInterface: true,
),
],
);
const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
categoryIdentifier: 'chat_category',
presentAlert: true,
presentBanner: true,
presentBadge: true,
presentList: true,
presentSound: true,
);
final String payload =
'{"committee_id": "$committeeId", "type": "chat", "sender_id": "${senderId ?? ""}", "sender_name": "${senderName ?? ""}"}';
await _localNotifications.show(
DateTime.now().millisecondsSinceEpoch.remainder(100000),
title,
body,
const NotificationDetails(android: androidDetails, iOS: iosDetails),
payload: payload,
);
}
/// Show onboarding reminder notification with Resume button
Future showOnboardingNotification({
required String title,
required String body,
String? step,
}) async {
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'onboarding_channel',
AppStrings.txtOnboardingNotifications,
channelDescription: AppStrings.txtNotificationsForOnboardingReminders,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
actions: [
AndroidNotificationAction(
'resume',
AppStrings.txtResume,
titleColor: Color(0xFF2196F3),
showsUserInterface: true,
),
],
);
const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
categoryIdentifier: 'onboarding_category',
presentAlert: true,
presentBanner: true,
presentBadge: true,
presentList: true,
presentSound: true,
);
final String payload = '{"type": "onboarding", "step": "${step ?? ""}"}';
await _localNotifications.show(
DateTime.now().millisecondsSinceEpoch.remainder(100000),
title,
body,
const NotificationDetails(android: androidDetails, iOS: iosDetails),
payload: payload,
);
}
/// Show change request notification with Accept/Reject buttons
Future showChangeRequestNotification({
required String title,
required String body,
required String committeeId,
String? requestId,
}) async {
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'change_request_channel',
AppStrings.txtChangeRequestNotifications,
channelDescription: AppStrings.txtNotificationsForChangeRequests,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
actions: [
AndroidNotificationAction(
'accept_change',
AppStrings.txtAccept,
titleColor: Color(0xFF2196F3),
showsUserInterface: true,
),
AndroidNotificationAction(
'reject_change',
AppStrings.txtReject,
titleColor: Color(0xFF757575),
showsUserInterface: true,
),
],
);
const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
categoryIdentifier: 'change_request_category',
presentAlert: true,
presentBanner: true,
presentBadge: true,
presentList: true,
presentSound: true,
);
final String payload =
'{"committee_id": "$committeeId", "type": "change_request", "request_id": "${requestId ?? ""}"}';
await _localNotifications.show(
DateTime.now().millisecondsSinceEpoch.remainder(100000),
title,
body,
const NotificationDetails(android: androidDetails, iOS: iosDetails),
payload: payload,
);
}
/// Handle FCM messages (foreground and background) and convert to local notifications with actions
Future handleForegroundMessage(RemoteMessage message) async {
try {
debugPrint('
debugPrint('
debugPrint(
'
);
final data = message.data;
final type = data['type'] ?? 'general';
final typeID =
(data['type_id'] ??
data['typeId'] ??
data['invitation_id'] ??
data['invitationId'] ??
'')
.toString();
debugPrint("Payload DATA -->$data");
// Get title and body from notification payload or data payload
final String title =
message.notification?.title ??
data['title'] ??
data['notification_title'] ??
AppStrings.txtNewNotification;
final String body =
message.notification?.body ??
data['body'] ??
data['notification_body'] ??
AppStrings.txtYouHaveNewNotification;
final committeeId = data['committee_id'] ?? '';
final committeeTitle = data['committee_title'] ?? AppStrings.txtCommitteeDefault;
debugPrint('
debugPrint('
debugPrint('
debugPrint('
debugPrint('
// Route to appropriate notification method based on type
switch (type) {
case 'invitation':
await showInvitationNotification(
title: title,
body: body,
committeeId: committeeId.toString(),
committeeTitle: committeeTitle,
invitationId: typeID.toString(),
);
break;
// case 'deposit_reminder':
case 'current_deposit_reminder':
await showDepositReminderNotification(
title: title,
body: body,
committeeId: committeeId.toString(),
);
break;
// case 'host_incomplete':
case 'pending_invitations':
case 'host_nudge_for_remind_to_join':
await showHostIncompleteNotification(
title: title,
body: body,
committeeId: committeeId.toString(),
type: type.toString(),
);
break;
// case 'host_rejection':
// case 'NOTIFICATION_INVITATION_REJECTED':
case 'invitee_interest_no':
case 'invitation_rejected':
await showHostRejectionNotification(
title: title,
body: body,
committeeId: committeeId.toString(),
type: type.toString(),
);
break;
// case 'host_interested':
case 'invitee_interest_yes':
await showHostInterestedNotification(
title: title,
body: body,
committeeId: committeeId.toString(),
);
break;
// case 'custom_push':
case 'custom_app_notifications':
final contactUsFlags = [
data['show_contact_us'],
data['contact_us_button'],
data['contact_us_enabled'],
data['add_contact_us'],
data['has_contact_us_cta'],
];
final hasExplicitContactUsFlag = contactUsFlags.any(
(value) => value != null && value.toString().trim().isNotEmpty,
);
// Backward compatible default: custom pushes show Contact Us unless
// backend explicitly sends one of the flags and all are false.
final showContactUsAction =
hasExplicitContactUsFlag ? contactUsFlags.any(_isTruthy) : true;
await showCustomPushNotification(
title: title,
body: body,
payload: data,
showContactUsAction: showContactUsAction,
);
break;
default:
// Show general notification without action buttons
await showGeneralNotification(
title: title,
body: body,
payload: data,
);
break;
}
debugPrint('
} catch (e, stackTrace) {
debugPrint('
debugPrint('
// Fallback: try to show a basic notification
try {
await showGeneralNotification(
title: message.notification?.title ?? AppStrings.txtNewNotification,
body: message.notification?.body ?? AppStrings.txtYouHaveNewNotification,
payload: message.data,
);
} catch (fallbackError) {
debugPrint('
}
}
}
/// Show a completion state for quick action API calls.
Future showActionResultNotification({
required int notificationId,
required String title,
required String body,
}) async {
if (!Platform.isAndroid) return;
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'invitation_channel',
AppStrings.txtInvitationNotifications,
channelDescription: AppStrings.txtNotificationsForCommitteeInvitations,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
onlyAlertOnce: true,
ongoing: false,
autoCancel: true,
);
await _localNotifications.show(
notificationId,
title,
body,
const NotificationDetails(android: androidDetails),
);
}
}
class SplashScreen extends StatefulWidget {
Future _navigateToNextScreen() async {
if (!mounted) return;
if (SplashScreen.isFromNotifications) {
final type = SplashScreen.notificationType;
final committeeId = SplashScreen.committeeID;
if (type == AppNotificationsType.NOTIFICATION_IHOST_NUDGE_TO_JOIN ||
type == AppNotificationsType.NOTIFICATION_PENDING_INVITES) {
SplashScreen.isFromNotificationRemindAll = true;
}
if (type == AppNotificationsType.NOTIFICATION_INVITATION_ACCEPTED ||
type == AppNotificationsType.NOTIFICATION_CURRENT_REMINDER ||
type == AppNotificationsType.NOTIFICATION_CONTACT_US ||
type == AppNotificationsType.NOTIFICATION_INVITATION_REJECTED ||
type == AppNotificationsType.NOTIFICATION_INVITEE_NO_NTEREST) {
// Treat accepted invitation the same as other lobby-related notifications
if (committeeId > 0) {
if (type == AppNotificationsType.NOTIFICATION_INVITATION_REJECTED ||
type == AppNotificationsType.NOTIFICATION_INVITEE_NO_NTEREST) {
SplashScreen.isFromNotificationLaunchInviteMoreUser = true;
}
if (type == AppNotificationsType.NOTIFICATION_CURRENT_REMINDER) {
SplashScreen.isFromAcceptInviteNotificationLaunchPayment = true;
}
if (type == AppNotificationsType.NOTIFICATION_PENDING_SETTLEMENTS ||
type ==
AppNotificationsType
.NOTIFICATION_PENDING_SETTLEMENTS_REMINDER ||
type ==
AppNotificationsType
.NOTIFICATION_PENDING_SETTLEMENTS_REMINDER_JOB) {
SplashScreen.isFromNotificationForSettlement = true;
}
SplashScreen.isFromAcceptInviteNotifications = true;
callForDashboard(clearStack: true);
} else {
callForDashboard(clearStack: true);
}
} else if (type == AppNotificationsType.NOTIFICATION_INVITATION ||
type == AppNotificationsType.NOTIFICATION_LOBBY ||
type == AppNotificationsType.NOTIFICATION_INVITATION_ACCEPTED ||
type == AppNotificationsType.NOTIFICATION_SWAP_REQUEST_ACCEPTED ||
type == AppNotificationsType.NOTIFICATION_COMMITTEE_CHAT ||
type == AppNotificationsType.NOTIFICATION_DISBAND_REQUEST ||
type == AppNotificationsType.NOTIFICATION_CONTACT_US ||
type == AppNotificationsType.NOTIFICATION_DISBAND_RESULT ||
type == AppNotificationsType.NOTIFICATION_INVITEE_NO_NTEREST ||
type == AppNotificationsType.NOTIFICATION_INVITATION_REJECTED ||
type == AppNotificationsType.NOTIFICATION_INVITEE_YES_NTEREST ||
type == AppNotificationsType.NOTIFICATION_IHOST_NUDGE_TO_JOIN ||
type == AppNotificationsType.NOTIFICATION_PENDING_INVITES ||
type == AppNotificationsType.NOTIFICATION_PENDING_SETTLEMENTS ||
type ==
AppNotificationsType.NOTIFICATION_PENDING_SETTLEMENTS_REMINDER ||
type ==
AppNotificationsType
.NOTIFICATION_PENDING_SETTLEMENTS_REMINDER_JOB ||
type == AppNotificationsType.NOTIFICATION_DISBAND_REQUEST_APPROVED) {
if (committeeId > 0) {
// Mark flag and let dashboard handle navigation when its context exists
SplashScreen.isFromAcceptInviteNotifications = true;
callForDashboard(clearStack: true);
} else {
// await callForLobby('471', clearStack: false);
callForDashboard(clearStack: true);
}
} else {
// await callForLobby('471', clearStack: true);
callForDashboard(clearStack: true);
}
return;
}
Navigator.of(
context,
).pushReplacement(MaterialPageRoute(builder: (_) => _getNextScreen()));
}
Future _setupFCM() async {
try {
// Get FCM token directly (no permission dialog)
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken != null) {
await saveStringToPreferences(Constants.fcmToken, fcmToken);
print('
}
// Listen for token refresh
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async {
await saveStringToPreferences(Constants.fcmToken, newToken);
print('
});
// Optional: handle background / terminated notifications
FirebaseMessaging.onMessageOpenedApp.listen((message) {
handleNotification(message);
});
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) handleNotification(message);
});
} catch (e) {
print('
}
initPlatformState();
}
void handleNotification(RemoteMessage message) {
final data = message.data;
final rawType = (data['type'] ?? '').toString();
final isContactUsType =
rawType == 'custom_app_notifications' ||
rawType == 'custom_push' ||
rawType == 'custom';
final type =
isContactUsType
? AppNotificationsType.NOTIFICATION_CONTACT_US
: rawType;
final committeeId = int.tryParse(data['committee_id'] ?? '');
SplashScreen.isFromNotifications = true;
SplashScreen.notificationType = type;
SplashScreen.committeeID = committeeId ?? 0;
if (isContactUsType) {
// Ensure dashboard consumes and opens Contact Us sheet after navigation.
saveBoolToPreferences(_pendingContactUsSheetKey, true);
}
}
Подробнее здесь: https://stackoverflow.com/questions/799 ... ing-on-ios
Мобильная версия