diff --git a/intercom_flutter/CHANGELOG.md b/intercom_flutter/CHANGELOG.md index 790bd86a..2b056f0e 100755 --- a/intercom_flutter/CHANGELOG.md +++ b/intercom_flutter/CHANGELOG.md @@ -4,6 +4,10 @@ * Bump Intercom Android SDK version to [18.10.0](https://github.com/intercom/intercom-android/releases/tag/18.10.0) * Bump Intercom iOS SDK version to [19.9.0](https://github.com/intercom/intercom-ios/releases/tag/19.9.0) +* Added API `setUserJwtProvider` so the SDK can request a fresh user JWT when the current one expires. Not supported on web. +* Added API `removeUserJwtProvider`. Not supported on web. +* Updated dependency `intercom_flutter_platform_interface: ^2.0.9` +* Updated dependency `intercom_flutter_web: ^1.1.14` ## 9.6.31 diff --git a/intercom_flutter/README.md b/intercom_flutter/README.md index fb2ffa84..5d43b379 100755 --- a/intercom_flutter/README.md +++ b/intercom_flutter/README.md @@ -150,6 +150,8 @@ But you can pre-define some Intercom settings, if you want (optional). - [ ] displayHelpCenterCollections - [ ] getWindowDidHideStream - [ ] suppressProactiveContent +- [ ] setUserJwtProvider +- [ ] removeUserJwtProvider ## Using Intercom keys with `--dart-define` 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 9e3da50e..31ef3fa3 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 @@ -1,6 +1,8 @@ package io.maido.intercom import android.app.Application +import android.os.Handler +import android.os.Looper import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -13,6 +15,7 @@ import io.intercom.android.sdk.* import io.intercom.android.sdk.identity.Registration import io.intercom.android.sdk.push.IntercomPushClient import io.intercom.android.sdk.ui.theme.ThemeMode +import java.util.concurrent.atomic.AtomicBoolean // No-op stream handler for windowDidHide event since it's only supported on iOS. class WindowDidHideStreamHandler : EventChannel.StreamHandler { @@ -33,10 +36,13 @@ class IntercomFlutterPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str private val intercomPushClient = IntercomPushClient() private var unreadConversationCountListener: UnreadConversationCountListener? = null + private var channel: MethodChannel? = null override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - val channel = MethodChannel(flutterPluginBinding.binaryMessenger, "maido.io/intercom") - channel.setMethodCallHandler(IntercomFlutterPlugin()) + val methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "maido.io/intercom") + val plugin = IntercomFlutterPlugin() + plugin.channel = methodChannel + methodChannel.setMethodCallHandler(plugin) val unreadEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "maido.io/intercom/unread") unreadEventChannel.setStreamHandler(IntercomFlutterPlugin()) val windowDidHideEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "maido.io/intercom/windowDidHide") @@ -281,6 +287,48 @@ class IntercomFlutterPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str result.success("Jwt added") } } + "setUserJwtProvider" -> { + val methodChannel = channel + Intercom.client().setUserJwtProvider(object : IntercomUserJwtProvider { + override fun provideUserJwt(callback: IntercomUserJwtCallback) { + if (methodChannel == null) { + callback.onFailure() + return + } + Handler(Looper.getMainLooper()).post { + val completed = AtomicBoolean(false) + methodChannel.invokeMethod("provideUserJwt", null, object : MethodChannel.Result { + override fun success(result: Any?) { + if (!completed.compareAndSet(false, true)) return + val jwt = result as? String + if (!jwt.isNullOrBlank()) { + callback.onSuccess(jwt) + } else { + callback.onFailure() + } + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + if (completed.compareAndSet(false, true)) { + callback.onFailure() + } + } + + override fun notImplemented() { + if (completed.compareAndSet(false, true)) { + callback.onFailure() + } + } + }) + } + } + }) + result.success("User JWT provider set") + } + "removeUserJwtProvider" -> { + Intercom.client().removeUserJwtProvider() + result.success("User JWT provider removed") + } "setAuthTokens" -> { val tokens = call.argument>("tokens") if (!tokens.isNullOrEmpty()) { diff --git a/intercom_flutter/ios/intercom_flutter/Sources/intercom_flutter/IntercomFlutterPlugin.m b/intercom_flutter/ios/intercom_flutter/Sources/intercom_flutter/IntercomFlutterPlugin.m index dc24a137..ca45830a 100644 --- a/intercom_flutter/ios/intercom_flutter/Sources/intercom_flutter/IntercomFlutterPlugin.m +++ b/intercom_flutter/ios/intercom_flutter/Sources/intercom_flutter/IntercomFlutterPlugin.m @@ -33,12 +33,16 @@ - (FlutterError*)onCancelWithArguments:(id)arguments { } @end -@implementation IntercomFlutterPlugin +@implementation IntercomFlutterPlugin { + FlutterMethodChannel *_channel; +} + + (void)registerWithRegistrar:(NSObject*)registrar { IntercomFlutterPlugin* instance = [[IntercomFlutterPlugin alloc] init]; FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"maido.io/intercom" binaryMessenger:[registrar messenger]]; + instance->_channel = channel; [registrar addMethodCallDelegate:instance channel:channel]; FlutterEventChannel* unreadChannel = [FlutterEventChannel eventChannelWithName:@"maido.io/intercom/unread" binaryMessenger:[registrar messenger]]; @@ -287,6 +291,31 @@ - (void) handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result NSString *jwt = call.arguments[@"jwt"]; [Intercom setUserJwt:jwt]; result(@"Jwt added"); + } else if([@"setUserJwtProvider" isEqualToString:call.method]) { + __weak FlutterMethodChannel *channel = _channel; + [Intercom setUserJwtProvider:^(void (^completion)(NSString * _Nullable jwt)) { + FlutterMethodChannel *strongChannel = channel; + if (strongChannel == nil) { + completion(nil); + return; + } + __block BOOL completed = NO; + [strongChannel invokeMethod:@"provideUserJwt" arguments:nil result:^(id result) { + if (completed) { + return; + } + completed = YES; + if ([result isKindOfClass:[NSString class]] && [(NSString *)result length] > 0) { + completion(result); + } else { + completion(nil); + } + }]; + }]; + result(@"User JWT provider set"); + } else if([@"removeUserJwtProvider" isEqualToString:call.method]) { + [Intercom removeUserJwtProvider]; + result(@"User JWT provider removed"); } else if([@"setAuthTokens" isEqualToString:call.method]) { NSDictionary *tokens = call.arguments[@"tokens"]; if (tokens != nil && [tokens count] > 0) { diff --git a/intercom_flutter/lib/intercom_flutter.dart b/intercom_flutter/lib/intercom_flutter.dart index 45c7ceb5..66df8cae 100755 --- a/intercom_flutter/lib/intercom_flutter.dart +++ b/intercom_flutter/lib/intercom_flutter.dart @@ -9,6 +9,8 @@ import 'package:intercom_flutter_platform_interface/intercom_status_callback.dar /// export the [IntercomVisibility] enum export 'package:intercom_flutter_platform_interface/enumeral.dart' show IntercomVisibility, IntercomTheme, IntercomProactiveContentType; +export 'package:intercom_flutter_platform_interface/intercom_flutter_platform_interface.dart' + show UserJwtProvider; export 'package:intercom_flutter_platform_interface/intercom_status_callback.dart' show IntercomStatusCallback, IntercomError; @@ -297,6 +299,35 @@ class Intercom { return IntercomFlutterPlatform.instance.setUserJwt(jwt); } + /// Registers a callback that supplies a fresh user JWT when the current one + /// is about to expire or has been rejected. + /// + /// The native SDK calls [provider] and retries the failed request once with + /// the returned token. Return `null` when a token cannot be supplied. + /// An empty string or a thrown error is treated the same as `null`. + /// + /// Set the first token with [setUserJwt] before login. This callback only + /// supplies later tokens. Call [removeUserJwtProvider] to stop asking. + /// + /// On iOS and Android, the SDK stops waiting after 10 seconds. This plugin + /// does not add its own timeout. + /// + /// Note: this is not supported on web. Call [setUserJwt] again to refresh + /// the token there. + Future setUserJwtProvider(UserJwtProvider provider) { + return IntercomFlutterPlatform.instance.setUserJwtProvider(provider); + } + + /// Stops asking for fresh user JWTs. + /// + /// Requests rejected for an expired JWT fail until [setUserJwt] is called + /// with a new token. + /// + /// Note: this is not supported on web. + Future removeUserJwtProvider() { + return IntercomFlutterPlatform.instance.removeUserJwtProvider(); + } + /// Set up the authentication (user-defined token) to secure your Data /// connectors. These tokens can be used for functionality such as Fin Actions. /// You can provide multiple tokens at once. Please ensure you have created diff --git a/intercom_flutter/pubspec.yaml b/intercom_flutter/pubspec.yaml index 6d9e36d1..694f5293 100644 --- a/intercom_flutter/pubspec.yaml +++ b/intercom_flutter/pubspec.yaml @@ -9,8 +9,8 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - intercom_flutter_platform_interface: ^2.0.8 - intercom_flutter_web: ^1.1.13 + intercom_flutter_platform_interface: ^2.0.9 + intercom_flutter_web: ^1.1.14 dev_dependencies: flutter_test: diff --git a/intercom_flutter/test/intercom_flutter_test.dart b/intercom_flutter/test/intercom_flutter_test.dart index 5b220860..9984915a 100755 --- a/intercom_flutter/test/intercom_flutter_test.dart +++ b/intercom_flutter/test/intercom_flutter_test.dart @@ -319,6 +319,16 @@ void main() { }); }); + test('setUserJwtProvider', () { + Intercom.instance.setUserJwtProvider(() async => 'fresh-jwt'); + expectMethodCall('setUserJwtProvider'); + }); + + test('removeUserJwtProvider', () { + Intercom.instance.removeUserJwtProvider(); + expectMethodCall('removeUserJwtProvider'); + }); + test('setAuthTokens', () { Intercom.instance.setAuthTokens({'security_token': 'test'}); expectMethodCall('setAuthTokens', arguments: { diff --git a/intercom_flutter_platform_interface/CHANGELOG.md b/intercom_flutter_platform_interface/CHANGELOG.md index df375615..a6043147 100755 --- a/intercom_flutter_platform_interface/CHANGELOG.md +++ b/intercom_flutter_platform_interface/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.0.9 + +* Added methods `setUserJwtProvider` and `removeUserJwtProvider`. + ## 2.0.8 * Added method `suppressProactiveContent`. 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 dd56e308..1d083d2d 100644 --- a/intercom_flutter_platform_interface/lib/intercom_flutter_platform_interface.dart +++ b/intercom_flutter_platform_interface/lib/intercom_flutter_platform_interface.dart @@ -3,6 +3,11 @@ import 'package:intercom_flutter_platform_interface/intercom_status_callback.dar import 'package:intercom_flutter_platform_interface/method_channel_intercom_flutter.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +/// Supplies a fresh user JWT when the native SDK asks for one. +/// +/// Return the new token, or `null` when one cannot be supplied. +typedef UserJwtProvider = Future Function(); + abstract class IntercomFlutterPlatform extends PlatformInterface { IntercomFlutterPlatform() : super(token: _token); @@ -287,6 +292,36 @@ abstract class IntercomFlutterPlatform extends PlatformInterface { throw UnimplementedError('setUserJwt() has not been implemented.'); } + /// Registers a callback that supplies a fresh user JWT when the current one + /// is about to expire or has been rejected. + /// + /// The native SDK calls [provider] and retries the failed request once with + /// the returned token. Return `null` when a token cannot be supplied. + /// An empty string or a thrown error is treated the same as `null`. + /// + /// Set the first token with [setUserJwt] before login. This callback only + /// supplies later tokens. Call [removeUserJwtProvider] to stop asking. + /// + /// On iOS and Android, the SDK stops waiting after 10 seconds. This plugin + /// does not add its own timeout. + /// + /// Note: this is not supported on web. Call [setUserJwt] again to refresh + /// the token there. + Future setUserJwtProvider(UserJwtProvider provider) { + throw UnimplementedError('setUserJwtProvider() has not been implemented.'); + } + + /// Stops asking for fresh user JWTs. + /// + /// Requests rejected for an expired JWT fail until [setUserJwt] is called + /// with a new token. + /// + /// Note: this is not supported on web. + Future removeUserJwtProvider() { + throw UnimplementedError( + 'removeUserJwtProvider() has not been implemented.'); + } + /// Set up the authentication (user-defined token) to secure your Data /// connectors. These tokens can be used for functionality such as Fin Actions. /// You can provide multiple tokens at once. Please ensure you have created 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 63af7d20..7207fd4c 100644 --- a/intercom_flutter_platform_interface/lib/method_channel_intercom_flutter.dart +++ b/intercom_flutter_platform_interface/lib/method_channel_intercom_flutter.dart @@ -10,6 +10,44 @@ const EventChannel _windowDidHideChannel = /// An implementation of [IntercomFlutterPlatform] that uses method channels. class MethodChannelIntercomFlutter extends IntercomFlutterPlatform { + static UserJwtProvider? _userJwtProvider; + static bool _isMethodCallHandlerSet = false; + + /// Registers the single handler for calls from native code on [_channel]. + /// + /// A channel holds only one handler, so every inbound method must be + /// dispatched from [_handleMethodCall] and registered only through here. + static void _ensureMethodCallHandler() { + if (_isMethodCallHandlerSet) return; + _channel.setMethodCallHandler(_handleMethodCall); + _isMethodCallHandlerSet = true; + } + + static Future _handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'provideUserJwt': + return _provideUserJwt(); + default: + throw MissingPluginException(); + } + } + + static Future _provideUserJwt() async { + final provider = _userJwtProvider; + if (provider == null) { + return null; + } + try { + final jwt = await provider(); + if (jwt == null || jwt.isEmpty) { + return null; + } + return jwt; + } catch (_) { + return null; + } + } + @override Future initialize( String appId, { @@ -261,6 +299,19 @@ class MethodChannelIntercomFlutter extends IntercomFlutterPlatform { await _channel.invokeMethod('setUserJwt', {'jwt': jwt}); } + @override + Future setUserJwtProvider(UserJwtProvider provider) async { + _userJwtProvider = provider; + _ensureMethodCallHandler(); + await _channel.invokeMethod('setUserJwtProvider'); + } + + @override + Future removeUserJwtProvider() async { + _userJwtProvider = null; + await _channel.invokeMethod('removeUserJwtProvider'); + } + @override Future setAuthTokens(Map tokens) async { await _channel.invokeMethod('setAuthTokens', {'tokens': tokens}); diff --git a/intercom_flutter_platform_interface/pubspec.yaml b/intercom_flutter_platform_interface/pubspec.yaml index c5b24f27..98612ad0 100644 --- a/intercom_flutter_platform_interface/pubspec.yaml +++ b/intercom_flutter_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: intercom_flutter_platform_interface description: A common platform interface for the intercom_flutter plugin. -version: 2.0.8 +version: 2.0.9 homepage: https://github.com/v3rm0n/intercom_flutter dependencies: diff --git a/intercom_flutter_platform_interface/test/method_channel_intercom_flutter_test.dart b/intercom_flutter_platform_interface/test/method_channel_intercom_flutter_test.dart index 97cca4a7..d9ea5895 100644 --- a/intercom_flutter_platform_interface/test/method_channel_intercom_flutter_test.dart +++ b/intercom_flutter_platform_interface/test/method_channel_intercom_flutter_test.dart @@ -34,14 +34,13 @@ void main() { group('$MethodChannelIntercomFlutter', () { const MethodChannel channel = MethodChannel('maido.io/intercom'); final List log = []; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + Future logOutboundMethodCall(MethodCall methodCall) async { log.add(methodCall); - - // Return null explicitly instead of relying on the implicit null - // returned by the method channel if no return statement is specified. return null; - }); + } + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, logOutboundMethodCall); final MethodChannelIntercomFlutter intercom = MethodChannelIntercomFlutter(); @@ -469,6 +468,75 @@ void main() { ); }); + test('setUserJwtProvider', () async { + await intercom.setUserJwtProvider(() async => 'fresh-jwt'); + expect( + log, + [isMethodCall('setUserJwtProvider', arguments: null)], + ); + }); + + test('provideUserJwt returns the provider value', () async { + await intercom.setUserJwtProvider(() async => 'fresh-jwt'); + final result = + await _invokeProvideUserJwt(channel, logOutboundMethodCall); + expect(result, 'fresh-jwt'); + }); + + test('provideUserJwt returns null when the provider returns null', + () async { + await intercom.setUserJwtProvider(() async => null); + final result = + await _invokeProvideUserJwt(channel, logOutboundMethodCall); + expect(result, isNull); + }); + + test('provideUserJwt returns null when the provider returns empty', + () async { + await intercom.setUserJwtProvider(() async => ''); + final result = + await _invokeProvideUserJwt(channel, logOutboundMethodCall); + expect(result, isNull); + }); + + test('provideUserJwt returns null when the provider throws', () async { + await intercom.setUserJwtProvider(() async => throw StateError('fail')); + final result = + await _invokeProvideUserJwt(channel, logOutboundMethodCall); + expect(result, isNull); + }); + + test('unknown inbound method is reported as not implemented', () async { + await intercom.setUserJwtProvider(() async => 'fresh-jwt'); + const codec = StandardMethodCodec(); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + messenger.setMockMethodCallHandler(channel, null); + try { + final reply = await messenger.handlePlatformMessage( + channel.name, + codec.encodeMethodCall(const MethodCall('unknownMethod')), + (_) {}, + ); + expect(reply, isNull); + } finally { + messenger.setMockMethodCallHandler(channel, logOutboundMethodCall); + } + }); + + test('removeUserJwtProvider', () async { + await intercom.setUserJwtProvider(() async => 'fresh-jwt'); + log.clear(); + await intercom.removeUserJwtProvider(); + expect( + log, + [isMethodCall('removeUserJwtProvider', arguments: null)], + ); + final result = + await _invokeProvideUserJwt(channel, logOutboundMethodCall); + expect(result, isNull); + }); + test('setAuthTokens', () async { await intercom.setAuthTokens({'security_token': 'test'}); expect( @@ -562,6 +630,32 @@ void main() { }); } +/// Simulates native → Dart `provideUserJwt`. Clears the outbound mock so the +/// inbound [MethodChannel.setMethodCallHandler] registered by +/// [MethodChannelIntercomFlutter] receives the message. +Future _invokeProvideUserJwt( + MethodChannel channel, + Future Function(MethodCall) restoreMockHandler, +) async { + const codec = StandardMethodCodec(); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + messenger.setMockMethodCallHandler(channel, null); + try { + final reply = await messenger.handlePlatformMessage( + channel.name, + codec.encodeMethodCall(const MethodCall('provideUserJwt')), + (_) {}, + ); + if (reply == null) { + return null; + } + return codec.decodeEnvelope(reply); + } finally { + messenger.setMockMethodCallHandler(channel, restoreMockHandler); + } +} + class IntercomFlutterPlatformMock extends Mock with MockPlatformInterfaceMixin implements IntercomFlutterPlatform {} diff --git a/intercom_flutter_web/CHANGELOG.md b/intercom_flutter_web/CHANGELOG.md index fa65940e..d4a0fff6 100755 --- a/intercom_flutter_web/CHANGELOG.md +++ b/intercom_flutter_web/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.1.14 + +* Updated `intercom_flutter_platform_interface` to `^2.0.9`. + ## 1.1.13 * Updated `intercom_flutter_platform_interface` to `^2.0.8`. diff --git a/intercom_flutter_web/README.md b/intercom_flutter_web/README.md index 1b899fba..407210e9 100755 --- a/intercom_flutter_web/README.md +++ b/intercom_flutter_web/README.md @@ -27,6 +27,8 @@ But you can pre-define some Intercom settings, if you want (optional), such as ` - displayHelpCenterCollections - getWindowDidHideStream - suppressProactiveContent +- setUserJwtProvider +- removeUserJwtProvider [1]: ../intercom_flutter diff --git a/intercom_flutter_web/pubspec.yaml b/intercom_flutter_web/pubspec.yaml index d5f0b407..f8bf7883 100644 --- a/intercom_flutter_web/pubspec.yaml +++ b/intercom_flutter_web/pubspec.yaml @@ -1,6 +1,6 @@ name: intercom_flutter_web description: Web platform implementation of intercom_flutter -version: 1.1.13 +version: 1.1.14 homepage: https://github.com/v3rm0n/intercom_flutter flutter: @@ -15,7 +15,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - intercom_flutter_platform_interface: ^2.0.8 + intercom_flutter_platform_interface: ^2.0.9 uuid: ^4.2.1 # to get the random uuid for loginUnidentifiedUser in web web: ^1.0.0