implement PN support on iOS & Android

This commit is contained in:
kanat
2023-05-09 15:09:16 -07:00
parent 1b874001fe
commit 13f923ab60
16 changed files with 208 additions and 26 deletions
@@ -44,5 +44,11 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- Set custom default icon. This is used when no icon is set for incoming notification messages.
See README(https://goo.gl/l4GJaQ) for more. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification" />
</application>
</manifest>
@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF"
android:autoMirrored="true">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
</group>
</vector>
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM9,11L7,11L7,9h2v2zM13,11h-2L11,9h2v2zM17,11h-2L15,9h2v2z"/>
</group>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

+149 -2
View File
@@ -8,6 +8,7 @@ import 'package:example/state/init_data.dart';
import 'package:example/utils/app_config.dart';
import 'package:example/utils/local_notification_observer.dart';
import 'package:example/utils/localizations.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@@ -15,11 +16,63 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
import 'firebase_options.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
debugPrint('[onBackgroundMessage] #firebase; message: ${message.toMap()}');
final data = message.data;
if (data['type'] != 'message.new') {
return;
}
String? apiKey, userId, token;
if (!kIsWeb) {
const secureStorage = FlutterSecureStorage();
apiKey = await secureStorage.read(key: kStreamApiKey);
userId = await secureStorage.read(key: kStreamUserId);
token = await secureStorage.read(key: kStreamToken);
}
debugPrint('[onBackgroundMessage] #firebase; apiKey: $apiKey, userId: $userId, token: $token');
if (userId == null || token == null) {
return;
}
final client = buildStreamChatClient(apiKey ?? kDefaultStreamApiKey);
final persistenceClient = StreamChatPersistenceClient();
await persistenceClient.connect(userId);
await client.connectUser(
User(id: userId),
token,
connectWebSocket: false,
);
final messageId = data['id'];
final cid = data['cid'];
final response = await client.getMessage(messageId);
debugPrint('[onBackgroundMessage] #firebase; response: $response');
await persistenceClient.updateMessages(cid, [response.message]);
debugPrint('[onBackgroundMessage] #firebase; saved');
}
Future<void> _firebaseMessagingForegroundHandler(RemoteMessage message) async {
debugPrint('[onForegroundMessage] #firebase; message: ${message.toMap()}');
}
Future<void> _firebaseMessagingOpenedHandler(RemoteMessage message) async {
debugPrint('[onOpenedMessage] #firebase; message: ${message.toMap()}');
}
final chatPersistentClient = StreamChatPersistenceClient(
logLevel: Level.SEVERE,
connectionMode: ConnectionMode.regular,
@@ -28,7 +81,7 @@ final chatPersistentClient = StreamChatPersistenceClient(
void _sampleAppLogHandler(LogRecord record) async {
if (kDebugMode) StreamChatClient.defaultLogHandler(record);
// report errors to setnry.io
// report errors to sentry.io
if (record.error != null || record.stackTrace != null) {
await Sentry.captureException(
record.error,
@@ -62,6 +115,9 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
with SplashScreenStateMixin, TickerProviderStateMixin {
final InitNotifier _initNotifier = InitNotifier();
final firebaseSubscriptions = <StreamSubscription<dynamic>>[];
StreamSubscription<String?>? userIdSubscription;
Future<InitData> _initConnection() async {
String? apiKey, userId, token;
@@ -71,7 +127,7 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
userId = await secureStorage.read(key: kStreamUserId);
token = await secureStorage.read(key: kStreamToken);
}
debugPrint('[initConnection] #firebase; apiKey: $apiKey, userId: $userId, token: $token');
final client = buildStreamChatClient(apiKey ?? kDefaultStreamApiKey);
if (userId != null && token != null) {
@@ -86,6 +142,77 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
return InitData(client, prefs);
}
Future<void> _initFirebaseMessaging(StreamChatClient client) async {
userIdSubscription?.cancel();
userIdSubscription = client.state.currentUserStream
.map((it) => it?.id)
.distinct()
.listen((userId) async {
debugPrint('[onUserIdSet] #firebase; userId: "$userId"');
if (userId != null) {
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler);
firebaseSubscriptions.add(FirebaseMessaging.onMessage
.listen(_firebaseMessagingForegroundHandler));
firebaseSubscriptions
.add(FirebaseMessaging.onMessageOpenedApp.listen((message) async {
debugPrint(
'[onOpenedMessage] #firebase; message: ${message.toMap()}');
final channelCid = (message.data['cid'] as String?) ?? '';
final channelType = (message.data['channel_type'] as String?) ?? '';
final channelId = (message.data['channel_id'] as String?) ?? '';
debugPrint('[onOpenedMessage] #firebase; channelCid; $channelCid, channelType: $channelType, channelId: $channelId');
var channel = client.state.channels[channelCid];
debugPrint('[onOpenedMessage] #firebase; channel1: $channel');
if (channel == null) {
channel = client.channel(
channelId,
id: channelId,
);
final state = await channel.watch();
debugPrint('[onOpenedMessage] #firebase; channelState: $state');
}
debugPrint('[onOpenedMessage] #firebase; channel2: $channel');
debugPrint('[onOpenedMessage] #firebase; #1');
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if (channel == null) {
debugPrint('[onOpenedMessage] #firebase; rejected (channel is null)');
return;
}
try {
debugPrint('[onOpenedMessage] #firebase; #2: ${_navigatorKey.currentContext}');
final router = GoRouter.of(_navigatorKey.currentContext!);
debugPrint('[onOpenedMessage] #firebase; #3: $router');
router.pushNamed(
Routes.CHANNEL_PAGE.name,
params: Routes.CHANNEL_PAGE.params(channel),
);
debugPrint('[onOpenedMessage] #firebase; #4');
} catch (e, stk) {
debugPrint('[onOpenedMessage] #firebase; failed: $e; $stk');
}
});
}));
firebaseSubscriptions.add(
FirebaseMessaging.instance.onTokenRefresh.listen((token) async {
debugPrint('[onTokenRefresh] #firebase; token: "$token"');
await client.addDevice(token, PushProvider.firebase);
debugPrint('[onTokenRefresh] #firebase; token set: $token');
}));
final token = await FirebaseMessaging.instance.getToken();
debugPrint('[initFirebaseMessaging] #firebase; token: "$token"');
if (token != null) {
await client.addDevice(token, PushProvider.firebase);
debugPrint('[initFirebaseMessaging] #firebase; token set: $token');
}
}
});
}
@override
void initState() {
final timeOfStartMs = DateTime.now().millisecondsSinceEpoch;
@@ -100,19 +227,38 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
if (now - timeOfStartMs > 1500) {
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
debugPrint('[forwardAnimations] #firebase; context1: $context');
debugPrint('[forwardAnimations] #firebase; _navigatorKey.currentContext1: ${_navigatorKey.currentContext}');
forwardAnimations();
final router = GoRouter.of(_navigatorKey.currentContext!);
debugPrint('[forwardAnimations] #firebase; router1: ${router}');
});
} else {
Future.delayed(const Duration(milliseconds: 1500)).then((value) {
debugPrint('[forwardAnimations] #firebase; context2: $context');
debugPrint('[forwardAnimations] #firebase; _navigatorKey.currentContext2: ${_navigatorKey.currentContext}');
forwardAnimations();
final router = GoRouter.of(_navigatorKey.currentContext!);
debugPrint('[forwardAnimations] #firebase; router2: ${router}');
});
}
_initFirebaseMessaging(initData.client);
},
);
super.initState();
}
@override
void dispose() {
super.dispose();
userIdSubscription?.cancel();
for (final subscription in firebaseSubscriptions) {
unawaited(subscription.cancel());
}
firebaseSubscriptions.clear();
}
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
LocalNotificationObserver? localNotificationObserver;
@@ -154,6 +300,7 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
@override
Widget build(BuildContext context) {
debugPrint('[AppState.build] #firebase; context: $context');
return Stack(
alignment: Alignment.center,
children: [
+7 -21
View File
@@ -1,35 +1,21 @@
import 'dart:async';
import 'package:example/app.dart';
import 'package:example/utils/app_config.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:example/app.dart';
import 'firebase_options.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
print("#firebase; PN_2 ");
await Firebase.initializeApp();
// If you're going to use other Firebase services in the background, such as Firestore,
// make sure you call `initializeApp` before using other Firebase services.
print('[setupPushNotifications] newMessage: ${message.toMap()}');
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
print("#firebase; PN_1");
await Firebase.initializeApp();
Firebase.apps.forEach((it) {
print("#firebase; app: $it");
});
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
debugPrint("#firebase; PN_1");
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
/// Captures errors reported by the Flutter framework.
FlutterError.onError = (FlutterErrorDetails details) {
if (kDebugMode) {
@@ -69,6 +69,7 @@ class _ChannelListPageState extends State<ChannelListPage> {
@override
Widget build(BuildContext context) {
print(">>>>>>>>>> ChannelListPage");
final user = StreamChat.of(context).currentUser;
if (user == null) {
return const Offstage();
@@ -76,8 +77,10 @@ class _ChannelListPageState extends State<ChannelListPage> {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: StreamChannelListHeader(
onNewChatButtonTap: () =>
GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name),
onNewChatButtonTap: () {
print(">>>>>>>>>> onNewChatButtonTap");
GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name);
},
preNavigationCallback: () =>
FocusScope.of(context).requestFocus(FocusNode()),
),
@@ -220,6 +223,7 @@ class LeftDrawer extends StatelessWidget {
.withOpacity(.5),
),
onTap: () {
print(">>>>>>>>>> NEW_CHAT");
Navigator.of(context).pop();
GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name);
},
@@ -238,6 +242,7 @@ class LeftDrawer extends StatelessWidget {
.withOpacity(.5),
),
onTap: () {
print(">>>>>>>>>> NEW_GROUP_CHAT");
Navigator.of(context).pop();
GoRouter.of(context).pushNamed(Routes.NEW_GROUP_CHAT.name);
},
@@ -253,6 +258,7 @@ class LeftDrawer extends StatelessWidget {
alignment: Alignment.bottomCenter,
child: ListTile(
onTap: () async {
print(">>>>>>>>>> CHOOSE_USER");
final client = StreamChat.of(context).client;
final router = GoRouter.of(context);
final initNotifier = context.read<InitNotifier>();
@@ -47,12 +47,14 @@ class _ChannelPageState extends State<ChannelPage> {
@override
Widget build(BuildContext context) {
print(">>>>>>>>>> CHANNEL PAGE");
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: StreamChannelHeader(
showTypingIndicator: false,
onBackPressed: () => GoRouter.of(context).pop(),
onImageTap: () async {
print(">>>>>>>>>> onImageTap");
final channel = StreamChannel.of(context).channel;
final router = GoRouter.of(context);
@@ -62,6 +64,7 @@ class _ChannelPageState extends State<ChannelPage> {
(element) => element.user!.id != currentUser!.id,
);
if (otherUser != null) {
print(">>>>>>>>>> GROUP_INFO_SCREEN otherUser");
router.pushNamed(
Routes.CHAT_INFO_SCREEN.name,
params: Routes.CHAT_INFO_SCREEN.params(channel),
@@ -69,6 +72,7 @@ class _ChannelPageState extends State<ChannelPage> {
);
}
} else {
print(">>>>>>>>>> GROUP_INFO_SCREEN");
GoRouter.of(context).pushNamed(
Routes.GROUP_INFO_SCREEN.name,
params: Routes.GROUP_INFO_SCREEN.params(channel),
@@ -21,7 +21,7 @@ void showLocalNotification(
if (event.message == null) return;
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const initializationSettingsAndroid =
AndroidInitializationSettings('launch_background');
AndroidInitializationSettings('ic_notification_in_app');
const initializationSettingsIOS = IOSInitializationSettings();
const initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
@@ -33,6 +33,8 @@ void showLocalNotification(
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onSelectNotification: (channelCid) async {
debugPrint("[onSelectNotification] #firebase; channelCid: $channelCid");
debugPrint("[onSelectNotification] #firebase; context: $context");
if (channelCid != null) {
final client = StreamChat.of(context).client;
final router = GoRouter.of(context);