From 0ed0975c82808003d4202608929bfc24d987de3d Mon Sep 17 00:00:00 2001 From: Allie Date: Tue, 17 Jun 2025 15:17:50 -0400 Subject: [PATCH] add stream listener wrapper for windowDidHide event --- intercom_flutter/README.md | 129 +++++++++++++----- .../maido/intercom/IntercomFlutterPlugin.kt | 24 +++- .../example/ios/Runner/Info.plist | 2 + intercom_flutter/example/lib/main.dart | 71 ++++++++-- .../ios/Classes/IntercomFlutterPlugin.h | 3 + .../ios/Classes/IntercomFlutterPlugin.m | 21 +++ intercom_flutter/lib/intercom_flutter.dart | 9 ++ .../test/intercom_flutter_test.dart | 28 ++++ .../intercom_flutter_platform_interface.dart | 10 ++ .../lib/method_channel_intercom_flutter.dart | 7 + 10 files changed, 253 insertions(+), 51 deletions(-) diff --git a/intercom_flutter/README.md b/intercom_flutter/README.md index 5c3e6397..f931b145 100755 --- a/intercom_flutter/README.md +++ b/intercom_flutter/README.md @@ -17,6 +17,7 @@ Flutter wrapper for Intercom [Android](https://github.com/intercom/intercom-andr Import `package:intercom_flutter/intercom_flutter.dart` and use the methods in `Intercom` class. Example: + ```dart import 'package:flutter/material.dart'; import 'package:intercom_flutter/intercom_flutter.dart'; @@ -50,6 +51,53 @@ class App extends StatelessWidget { See Intercom [Android](https://developers.intercom.com/installing-intercom/docs/intercom-for-android) and [iOS](https://developers.intercom.com/installing-intercom/docs/intercom-for-ios) package documentation for more information. +### Listening for Intercom Window Events + +You can listen for when the Intercom window is hidden (closed) using the `getWindowDidHideStream()` method. This is particularly useful for performing actions when users close the Intercom messenger, help center, or other Intercom windows. + +**Note:** This feature is only available on iOS. + +````dart +import 'package:flutter/material.dart'; +import 'package:intercom_flutter/intercom_flutter.dart'; +import 'dart:async'; + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + StreamSubscription? _windowDidHideSubscription; + + @override + void initState() { + super.initState(); + // Listen for when the Intercom window is hidden + _windowDidHideSubscription = Intercom.instance.getWindowDidHideStream().listen((_) { + // This will be called when the Intercom window is closed + print('Intercom window was closed!'); + // Perform any actions you need when the window is closed + }); + } + + @override + void dispose() { + _windowDidHideSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FlatButton( + child: Text('Open Intercom'), + onPressed: () async { + await Intercom.instance.displayMessenger(); + }, + ); + } +} + ### Android Make sure that your app's `MainActivity` extends `FlutterFragmentActivity` (you can check the example). @@ -57,7 +105,7 @@ Make sure that your app's `MainActivity` extends `FlutterFragmentActivity` (you Permissions: ```xml -``` +```` Optional permissions: @@ -75,25 +123,32 @@ android.enableJetifier=true ``` According to the documentation, Intercom must be initialized in the Application onCreate. So follow the below steps to achieve the same: + - Setup custom application class if you don't have any. - - Create a custom `android.app.Application` class named `MyApp`. - - Add an `onCreate()` override. The class should look like this: - ```kotlin - import android.app.Application - class MyApp: Application() { + - Create a custom `android.app.Application` class named `MyApp`. + - Add an `onCreate()` override. The class should look like this: + + ```kotlin + import android.app.Application + + class MyApp: Application() { + + override fun onCreate() { + super.onCreate() + } + } + ``` + + - Open your `AndroidManifest.xml` and find the `application` tag. In it, add an `android:name` attribute, and set the value to your class' name, prefixed by a dot (.). + + ```xml + + ``` - override fun onCreate() { - super.onCreate() - } - } - ``` - - Open your `AndroidManifest.xml` and find the `application` tag. In it, add an `android:name` attribute, and set the value to your class' name, prefixed by a dot (.). - ```xml - - ``` - Now initialize the Intercom SDK inside the `onCreate()` of custom application class according to the following: + ```kotlin import android.app.Application import io.maido.intercom.IntercomFlutterPlugin @@ -109,16 +164,18 @@ class MyApp : Application() { ``` ### iOS + Make sure that you have a `NSPhotoLibraryUsageDescription` entry in your `Info.plist`. ### Push notifications setup + This plugin works in combination with the [`firebase_messaging`](https://pub.dev/packages/firebase_messaging) plugin to receive Push Notifications. To set this up: -* First, implement [`firebase_messaging`](https://pub.dev/packages/firebase_messaging) -* Then, add the Firebase server key to Intercom, as described [here](https://developers.intercom.com/installing-intercom/docs/android-fcm-push-notifications#section-step-3-add-your-server-key-to-intercom-for-android-settings) (you can skip 1 and 2 as you have probably done them while configuring `firebase_messaging`) -* Follow the steps as described [here](https://developers.intercom.com/installing-intercom/docs/ios-push-notifications) to enable push notification in iOS. -* Starting from Android 13 you may need to ask for notification permissions (as of version 13 `firebase_messaging` should support that) -* Ask FirebaseMessaging for the token that we need to send to Intercom, and give it to Intercom (so Intercom can send push messages to the correct device), please note that in order to receive push notifications in your iOS app, you have to send the APNS token to Intercom. The example below uses [`firebase_messaging`](https://pub.dev/packages/firebase_messaging) to get either the FCM or APNS token based on the platform: +- First, implement [`firebase_messaging`](https://pub.dev/packages/firebase_messaging) +- Then, add the Firebase server key to Intercom, as described [here](https://developers.intercom.com/installing-intercom/docs/android-fcm-push-notifications#section-step-3-add-your-server-key-to-intercom-for-android-settings) (you can skip 1 and 2 as you have probably done them while configuring `firebase_messaging`) +- Follow the steps as described [here](https://developers.intercom.com/installing-intercom/docs/ios-push-notifications) to enable push notification in iOS. +- Starting from Android 13 you may need to ask for notification permissions (as of version 13 `firebase_messaging` should support that) +- Ask FirebaseMessaging for the token that we need to send to Intercom, and give it to Intercom (so Intercom can send push messages to the correct device), please note that in order to receive push notifications in your iOS app, you have to send the APNS token to Intercom. The example below uses [`firebase_messaging`](https://pub.dev/packages/firebase_messaging) to get either the FCM or APNS token based on the platform: ```dart final firebaseMessaging = FirebaseMessaging.instance; @@ -130,15 +187,18 @@ Intercom.instance.sendTokenToIntercom(intercomToken); Now, if either Firebase direct (e.g. by your own backend server) or Intercom sends you a message, it will be delivered to your app. ### Web + You don't need to do any extra steps for the web. Intercom script will be automatically injected. But you can pre-define some Intercom settings, if you want (optional). + ```html ``` + #### Following functions are not yet supported on Web: - [ ] unreadConversationCount @@ -149,18 +209,22 @@ But you can pre-define some Intercom settings, if you want (optional). - [ ] handlePush - [ ] displayCarousel - [ ] displayHelpCenterCollections +- [ ] getWindowDidHideStream ## Using Intercom keys with `--dart-define` -Use `--dart-define` variables to avoid hardcoding Intercom keys. +Use `--dart-define` variables to avoid hardcoding Intercom keys. ### Pass the Intercom keys with `flutter run` or `flutter build` command using `--dart-define`. + ```dart flutter run --dart-define="INTERCOM_APP_ID=appID" --dart-define="INTERCOM_ANDROID_KEY=androidKey" --dart-define="INTERCOM_IOS_KEY=iosKey" ``` + Note: You can also use `--dart-define-from-file` which is introduced in Flutter 3.7. ### Reading keys in Dart side and initialize the SDK. + ```dart String appId = String.fromEnvironment("INTERCOM_APP_ID", ""); String androidKey = String.fromEnvironment("INTERCOM_ANDROID_KEY", ""); @@ -171,7 +235,8 @@ Intercom.instance.initialize(appId, iosApiKey: iOSKey, androidApiKey: androidKey ### Reading keys in Android native side and initialize the SDK. -* Add the following code to `build.gradle`. +- Add the following code to `build.gradle`. + ``` def dartEnvironmentVariables = [] if (project.hasProperty('dart-defines')) { @@ -184,7 +249,8 @@ if (project.hasProperty('dart-defines')) { } ``` -* Place `dartEnvironmentVariables` inside the build config +- Place `dartEnvironmentVariables` inside the build config + ``` defaultConfig { ... @@ -193,7 +259,8 @@ defaultConfig { } ``` -* Read the build config fields +- Read the build config fields + ```kotlin import android.app.Application import android.os.Build @@ -202,10 +269,10 @@ import io.maido.intercom.IntercomFlutterPlugin class MyApp : Application() { override fun onCreate() { super.onCreate() - + // Add this line with your keys - IntercomFlutterPlugin.initSdk(this, - appId = BuildConfig.INTERCOM_APP_ID, + IntercomFlutterPlugin.initSdk(this, + appId = BuildConfig.INTERCOM_APP_ID, androidApiKey = BuildConfig.INTERCOM_ANDROID_KEY) } } diff --git a/intercom_flutter/android/src/main/kotlin/io/maido/intercom/IntercomFlutterPlugin.kt b/intercom_flutter/android/src/main/kotlin/io/maido/intercom/IntercomFlutterPlugin.kt index c952c17e..8e8b6323 100644 --- a/intercom_flutter/android/src/main/kotlin/io/maido/intercom/IntercomFlutterPlugin.kt +++ b/intercom_flutter/android/src/main/kotlin/io/maido/intercom/IntercomFlutterPlugin.kt @@ -32,6 +32,8 @@ class IntercomFlutterPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str channel.setMethodCallHandler(IntercomFlutterPlugin()) val unreadEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "maido.io/intercom/unread") unreadEventChannel.setStreamHandler(IntercomFlutterPlugin()) + val windowDidHideEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "maido.io/intercom/windowDidHide") + windowDidHideEventChannel.setStreamHandler(IntercomFlutterPlugin()) application = flutterPluginBinding.applicationContext as Application } @@ -366,14 +368,22 @@ class IntercomFlutterPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str } override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - unreadConversationCountListener = UnreadConversationCountListener { count -> - val handler = android.os.Handler(android.os.Looper.getMainLooper()) - handler.post { - events?.success(count) + // Check if this is the unread stream or windowDidHide stream + // For unread stream, we set up the listener + // For windowDidHide stream, we don't set up anything since it's iOS-specific + if (arguments == null || arguments.toString().contains("unread") || arguments.toString().isEmpty()) { + // This is the unread stream + unreadConversationCountListener = UnreadConversationCountListener { count -> + val handler = android.os.Handler(android.os.Looper.getMainLooper()) + handler.post { + events?.success(count) + } + }.also { + Intercom.client().addUnreadConversationCountListener(it) } - }.also { - Intercom.client().addUnreadConversationCountListener(it) - } + } + // For windowDidHide stream, we don't need to do anything since it's iOS-specific + // The stream will not emit any events on Android } override fun onCancel(arguments: Any?) { diff --git a/intercom_flutter/example/ios/Runner/Info.plist b/intercom_flutter/example/ios/Runner/Info.plist index abc92b33..37956cfb 100644 --- a/intercom_flutter/example/ios/Runner/Info.plist +++ b/intercom_flutter/example/ios/Runner/Info.plist @@ -43,5 +43,7 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + diff --git a/intercom_flutter/example/lib/main.dart b/intercom_flutter/example/lib/main.dart index 69ac660f..e2a0e454 100644 --- a/intercom_flutter/example/lib/main.dart +++ b/intercom_flutter/example/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:intercom_flutter/intercom_flutter.dart'; @@ -13,7 +15,39 @@ void main() async { runApp(SampleApp()); } -class SampleApp extends StatelessWidget { +class SampleApp extends StatefulWidget { + @override + _SampleAppState createState() => _SampleAppState(); +} + +class _SampleAppState extends State { + StreamSubscription? _windowDidHideSubscription; + + @override + void initState() { + super.initState(); + // Listen for when the Intercom window is hidden + _windowDidHideSubscription = + Intercom.instance.getWindowDidHideStream().listen((_) { + // This will be called when the Intercom window is closed + // Only works on iOS + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Intercom window was closed!'), + duration: Duration(seconds: 2), + ), + ); + } + }); + } + + @override + void dispose() { + _windowDidHideSubscription?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -22,18 +56,29 @@ class SampleApp extends StatelessWidget { title: const Text('Intercom example app'), ), body: Center( - child: TextButton( - onPressed: () { - // NOTE: - // Messenger will load the messages only if the user is registered - // in Intercom. - // Either identified or unidentified. - // So make sure to login the user in Intercom first before opening - // the intercom messenger. - // Otherwise messenger will not load. - Intercom.instance.displayMessenger(); - }, - child: Text('Show messenger'), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { + // NOTE: + // Messenger will load the messages only if the user is registered + // in Intercom. + // Either identified or unidentified. + // So make sure to login the user in Intercom first before opening + // the intercom messenger. + // Otherwise messenger will not load. + Intercom.instance.displayMessenger(); + }, + child: Text('Show messenger'), + ), + SizedBox(height: 20), + Text( + 'Close the Intercom window to see the notification!', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16), + ), + ], ), ), ), diff --git a/intercom_flutter/ios/Classes/IntercomFlutterPlugin.h b/intercom_flutter/ios/Classes/IntercomFlutterPlugin.h index 67677316..f6dd6067 100644 --- a/intercom_flutter/ios/Classes/IntercomFlutterPlugin.h +++ b/intercom_flutter/ios/Classes/IntercomFlutterPlugin.h @@ -5,3 +5,6 @@ @interface UnreadStreamHandler : NSObject @end + +@interface WindowDidHideStreamHandler : NSObject +@end diff --git a/intercom_flutter/ios/Classes/IntercomFlutterPlugin.m b/intercom_flutter/ios/Classes/IntercomFlutterPlugin.m index a77eea3d..2ea3f34c 100644 --- a/intercom_flutter/ios/Classes/IntercomFlutterPlugin.m +++ b/intercom_flutter/ios/Classes/IntercomFlutterPlugin.m @@ -2,6 +2,7 @@ #import id unread; +id windowDidHide; @implementation UnreadStreamHandler - (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { @@ -18,6 +19,20 @@ - (FlutterError*)onCancelWithArguments:(id)arguments { } @end +@implementation WindowDidHideStreamHandler +- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { + windowDidHide = [[NSNotificationCenter defaultCenter] addObserverForName:IntercomWindowDidHideNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { + eventSink(@(YES)); + }]; + return nil; +} + +- (FlutterError*)onCancelWithArguments:(id)arguments { + [[NSNotificationCenter defaultCenter] removeObserver:windowDidHide]; + return nil; +} +@end + @implementation IntercomFlutterPlugin + (void)registerWithRegistrar:(NSObject*)registrar { IntercomFlutterPlugin* instance = [[IntercomFlutterPlugin alloc] init]; @@ -31,6 +46,12 @@ + (void)registerWithRegistrar:(NSObject*)registrar { [[UnreadStreamHandler alloc] init]; [unreadChannel setStreamHandler:unreadStreamHandler]; + FlutterEventChannel* windowDidHideChannel = [FlutterEventChannel eventChannelWithName:@"maido.io/intercom/windowDidHide" + binaryMessenger:[registrar messenger]]; + WindowDidHideStreamHandler* windowDidHideStreamHandler = + [[WindowDidHideStreamHandler alloc] init]; + [windowDidHideChannel setStreamHandler:windowDidHideStreamHandler]; + } - (void) handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result{ diff --git a/intercom_flutter/lib/intercom_flutter.dart b/intercom_flutter/lib/intercom_flutter.dart index b4b794bf..859472f0 100755 --- a/intercom_flutter/lib/intercom_flutter.dart +++ b/intercom_flutter/lib/intercom_flutter.dart @@ -48,6 +48,15 @@ class Intercom { return IntercomFlutterPlatform.instance.getUnreadStream(); } + /// You can listen for when the Intercom window is hidden. + /// + /// This stream emits when the Intercom window (messenger, help center, etc.) is closed. + /// This allows developers to perform certain actions in their app when the Intercom window is closed. + /// Only available on iOS. + Stream getWindowDidHideStream() { + return IntercomFlutterPlatform.instance.getWindowDidHideStream(); + } + /// This method allows you to set a fixed bottom padding for in app messages and the launcher. /// /// It is useful if your app has a tab bar or similar UI at the bottom of your window. diff --git a/intercom_flutter/test/intercom_flutter_test.dart b/intercom_flutter/test/intercom_flutter_test.dart index 5239c267..e9090683 100755 --- a/intercom_flutter/test/intercom_flutter_test.dart +++ b/intercom_flutter/test/intercom_flutter_test.dart @@ -242,6 +242,34 @@ void main() { }); }); + group('WindowDidHide', () { + const String channelName = 'maido.io/intercom/windowDidHide'; + const MethodChannel channel = MethodChannel(channelName); + final bool value = true; + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + channelName, + const StandardMethodCodec().encodeSuccessEnvelope(value), + (ByteData? data) {}, + ); + return; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('testStream', () async { + expect(await Intercom.instance.getWindowDidHideStream().first, value); + }); + }); + test('displayArticle', () async { final String testArticleId = "123456"; await Intercom.instance.displayArticle(testArticleId); diff --git a/intercom_flutter_platform_interface/lib/intercom_flutter_platform_interface.dart b/intercom_flutter_platform_interface/lib/intercom_flutter_platform_interface.dart index 4f5b7494..f90b53c8 100644 --- a/intercom_flutter_platform_interface/lib/intercom_flutter_platform_interface.dart +++ b/intercom_flutter_platform_interface/lib/intercom_flutter_platform_interface.dart @@ -47,6 +47,16 @@ abstract class IntercomFlutterPlatform extends PlatformInterface { throw UnimplementedError('getUnreadStream() has not been implemented.'); } + /// You can listen for when the Intercom window is hidden. + /// + /// This stream emits when the Intercom window (messenger, help center, etc.) is closed. + /// This allows developers to perform certain actions in their app when the Intercom window is closed. + /// Only available on iOS. + Stream getWindowDidHideStream() { + throw UnimplementedError( + 'getWindowDidHideStream() has not been implemented.'); + } + /// To make sure that conversations between you and your users are kept private /// and that one user can't impersonate another then you need you need to setup /// the identity verification. diff --git a/intercom_flutter_platform_interface/lib/method_channel_intercom_flutter.dart b/intercom_flutter_platform_interface/lib/method_channel_intercom_flutter.dart index 949a8c50..c511a84c 100644 --- a/intercom_flutter_platform_interface/lib/method_channel_intercom_flutter.dart +++ b/intercom_flutter_platform_interface/lib/method_channel_intercom_flutter.dart @@ -4,6 +4,8 @@ import 'package:intercom_flutter_platform_interface/intercom_status_callback.dar const MethodChannel _channel = MethodChannel('maido.io/intercom'); const EventChannel _unreadChannel = EventChannel('maido.io/intercom/unread'); +const EventChannel _windowDidHideChannel = + EventChannel('maido.io/intercom/windowDidHide'); /// An implementation of [IntercomFlutterPlatform] that uses method channels. class MethodChannelIntercomFlutter extends IntercomFlutterPlatform { @@ -25,6 +27,11 @@ class MethodChannelIntercomFlutter extends IntercomFlutterPlatform { return _unreadChannel.receiveBroadcastStream(); } + @override + Stream getWindowDidHideStream() { + return _windowDidHideChannel.receiveBroadcastStream(); + } + @override Future setUserHash(String userHash) async { await _channel.invokeMethod('setUserHash', {'userHash': userHash});