Skip to content

Commit 546b5c1

Browse files
authored
🐛 Always terminate 'authenticate' callbacks on Android (#28)
* Fix #26 AndroidX build issues - enables androidX and jetifier for non-androidx libs - also upgrades Gradle wrappers to 6.3 - also upgrade Android Gradle plugins to 3.6.3 - also sets Kotlin version of both lib + example to 1.3.72 * Terminate all dangling authenticate calls on Android - If custom tabs are closed on Android, the Future of `authenticated` function will never complete. We can solve that by registering a WidgetsBindingObserver before the actual call happens. If the app is resumed we can then look for all active callbacks, and remove them (because otherwise they would never complete). - We remove the WidgetsBindingObserver again each time, because that could have side-effects on the calling app. - Also updated the sample code to reflect that `authenticate` can and will fail with a PlatformException when the user aborts. - Also cleaned up the Kotlin code a bit because this was fun :-) * Rename callback into danglingResultCallback - improves readability, as there was *another* callback with the same name in the same scope
1 parent 0085d84 commit 546b5c1

File tree

6 files changed

+78
-34
lines changed

6 files changed

+78
-34
lines changed

android/src/main/kotlin/com/linusu/flutter_web_auth/CallbackActivity.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ import android.app.Activity
44
import android.net.Uri
55
import android.os.Bundle
66

7-
public class CallbackActivity: Activity() {
7+
class CallbackActivity: Activity() {
88
override fun onCreate(savedInstanceState: Bundle?) {
99
super.onCreate(savedInstanceState)
1010

11-
val url = getIntent()?.getData() as? Uri
12-
val scheme = url?.getScheme()
11+
val url = intent?.data
12+
val scheme = url?.scheme
1313

1414
if (scheme != null) {
1515
FlutterWebAuthPlugin.callbacks.remove(scheme)?.success(url.toString())
1616
}
1717

18-
this.finish()
18+
finish()
1919
}
2020
}
Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.linusu.flutter_web_auth
22

3-
import java.util.HashMap
4-
53
import android.content.Context
64
import android.content.Intent
75
import android.net.Uri
@@ -16,7 +14,7 @@ import io.flutter.plugin.common.PluginRegistry.Registrar
1614

1715
class FlutterWebAuthPlugin(private val context: Context): MethodCallHandler {
1816
companion object {
19-
public val callbacks = HashMap<String, Result>()
17+
val callbacks = mutableMapOf<String, Result>()
2018

2119
@JvmStatic
2220
fun registerWith(registrar: Registrar) {
@@ -25,22 +23,30 @@ class FlutterWebAuthPlugin(private val context: Context): MethodCallHandler {
2523
}
2624
}
2725

28-
override fun onMethodCall(call: MethodCall, result: Result) {
29-
if (call.method == "authenticate") {
30-
val url = Uri.parse(call.argument<String>("url"))
31-
val callbackUrlScheme = call.argument<String>("callbackUrlScheme")!!
32-
33-
callbacks.put(callbackUrlScheme, result)
34-
35-
val intent = CustomTabsIntent.Builder().build()
36-
val keepAliveIntent = Intent().setClassName(context.getPackageName(), KeepAliveService::class.java.canonicalName)
37-
38-
intent.intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_TASK)
39-
intent.intent.putExtra("android.support.customtabs.extra.KEEP_ALIVE", keepAliveIntent)
40-
41-
intent.launchUrl(context, url)
42-
} else {
43-
result.notImplemented()
26+
override fun onMethodCall(call: MethodCall, resultCallback: Result) {
27+
when (call.method) {
28+
"authenticate" -> {
29+
val url = Uri.parse(call.argument("url"))
30+
val callbackUrlScheme = call.argument<String>("callbackUrlScheme")!!
31+
32+
callbacks[callbackUrlScheme] = resultCallback
33+
34+
val intent = CustomTabsIntent.Builder().build()
35+
val keepAliveIntent = Intent(context, KeepAliveService::class.java)
36+
37+
intent.intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_TASK)
38+
intent.intent.putExtra("android.support.customtabs.extra.KEEP_ALIVE", keepAliveIntent)
39+
40+
intent.launchUrl(context, url)
41+
}
42+
"cleanUpDanglingCalls" -> {
43+
callbacks.forEach{ (_, danglingResultCallback) ->
44+
danglingResultCallback.error("CANCELED", "User canceled login", null)
45+
}
46+
callbacks.clear()
47+
resultCallback.success(null)
48+
}
49+
else -> resultCallback.notImplemented()
4450
}
4551
}
4652
}

android/src/main/kotlin/com/linusu/flutter_web_auth/KeepAliveService.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import android.content.Intent
55
import android.os.Binder
66
import android.os.IBinder
77

8-
public class KeepAliveService: Service() {
8+
class KeepAliveService: Service() {
99
companion object {
10-
val sBinder = Binder()
10+
val binder = Binder()
1111
}
1212

1313
override fun onBind(intent: Intent): IBinder {
14-
return sBinder
14+
return binder
1515
}
1616
}

example/lib/main.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter/services.dart';
23
import 'dart:async';
34
import 'dart:io' show HttpServer;
45

@@ -93,9 +94,12 @@ class _MyAppState extends State<MyApp> {
9394
final url = 'http://localtest.me:43823/';
9495
final callbackUrlScheme = 'foobar';
9596

96-
final result = await FlutterWebAuth.authenticate(url: url, callbackUrlScheme: callbackUrlScheme);
97-
98-
setState(() { _status = 'Got result: $result'; });
97+
try {
98+
final result = await FlutterWebAuth.authenticate(url: url, callbackUrlScheme: callbackUrlScheme);
99+
setState(() { _status = 'Got result: $result'; });
100+
} on PlatformException catch (e) {
101+
setState(() { _status = 'Got error: $e'; });
102+
}
99103
}
100104

101105
@override

ios/Classes/SwiftFlutterWebAuthPlugin.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ public class SwiftFlutterWebAuthPlugin: NSObject, FlutterPlugin {
1515
let url = URL(string: (call.arguments as! Dictionary<String, AnyObject>)["url"] as! String)!
1616
let callbackURLScheme = (call.arguments as! Dictionary<String, AnyObject>)["callbackUrlScheme"] as! String
1717

18-
var keepMe: Any? = nil
18+
var sessionToKeepAlive: Any? = nil // if we do not keep the session alive, it will get closed immediately while showing the dialog
1919
let completionHandler = { (url: URL?, err: Error?) in
20-
keepMe = nil
20+
sessionToKeepAlive = nil
2121

2222
if let err = err {
2323
if #available(iOS 12, *) {
@@ -52,12 +52,15 @@ public class SwiftFlutterWebAuthPlugin: NSObject, FlutterPlugin {
5252
}
5353

5454
session.start()
55-
keepMe = session
55+
sessionToKeepAlive = session
5656
} else {
5757
let session = SFAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme, completionHandler: completionHandler)
5858
session.start()
59-
keepMe = session
59+
sessionToKeepAlive = session
6060
}
61+
} else if (call.method == "cleanUpDanglingCalls") {
62+
// we do not keep track of old callbacks on iOS, so nothing to do here
63+
result(nil)
6164
} else {
6265
result(FlutterMethodNotImplemented)
6366
}

lib/flutter_web_auth.dart

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,48 @@
11
import 'dart:async';
22

3+
import 'package:flutter/cupertino.dart';
34
import 'package:flutter/foundation.dart' show required;
45
import 'package:flutter/services.dart' show MethodChannel;
56

7+
class _OnAppLifecycleResumeObserver extends WidgetsBindingObserver {
8+
final Function onResumed;
9+
10+
_OnAppLifecycleResumeObserver(this.onResumed);
11+
12+
@override
13+
void didChangeAppLifecycleState(AppLifecycleState state) {
14+
if (state == AppLifecycleState.resumed) {
15+
onResumed();
16+
}
17+
}
18+
}
19+
620
class FlutterWebAuth {
721
static const MethodChannel _channel = const MethodChannel('flutter_web_auth');
822

23+
static final _OnAppLifecycleResumeObserver _resumedObserver = _OnAppLifecycleResumeObserver(() {
24+
_cleanUpDanglingCalls(); // unawaited
25+
});
26+
927
/// Ask the user to authenticate to the specified web service.
1028
///
1129
/// The page pointed to by [url] will be loaded and displayed to the user. From the page, the user can authenticate herself and grant access to the app. On completion, the service will send a callback URL with an authentication token, and this URL will be result of the returned [Future].
1230
///
1331
/// [callbackUrlScheme] should be a string specifying the scheme of the url that the page will redirect to upon successful authentication.
1432
static Future<String> authenticate({@required String url, @required String callbackUrlScheme}) async {
15-
return await _channel.invokeMethod('authenticate', <String, dynamic>{'url': url, 'callbackUrlScheme': callbackUrlScheme}) as String;
33+
WidgetsBinding.instance.removeObserver(_resumedObserver); // safety measure so we never add this observer twice
34+
WidgetsBinding.instance.addObserver(_resumedObserver);
35+
return await _channel.invokeMethod('authenticate', <String, dynamic>{
36+
'url': url,
37+
'callbackUrlScheme': callbackUrlScheme,
38+
}) as String;
39+
}
40+
41+
/// On Android, the plugin has to store the Result callbacks in order to pass the result back to the caller of
42+
/// `authenticate`. But if that result never comes the callback will dangle around forever. This can be called to
43+
/// terminate all `authenticate` calls with an error.
44+
static Future<void> _cleanUpDanglingCalls() async {
45+
await _channel.invokeMethod('cleanUpDanglingCalls');
46+
WidgetsBinding.instance.removeObserver(_resumedObserver);
1647
}
1748
}

0 commit comments

Comments
 (0)