diff --git a/android/build.gradle b/android/build.gradle index 4fda61f..8616d91 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'com.linusu.flutter_web_auth' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.72' + ext.kotlin_version = '1.5.21' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + classpath 'com.android.tools.build:gradle:3.6.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -17,7 +17,7 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -25,13 +25,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 16 + minSdkVersion 23 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } lintOptions { @@ -39,7 +39,7 @@ android { } dependencies { - implementation 'androidx.browser:browser:1.0.0' + implementation 'androidx.browser:browser:1.3.0' } } diff --git a/android/src/main/kotlin/com/linusu/flutter_web_auth/CallbackActivity.kt b/android/src/main/kotlin/com/linusu/flutter_web_auth/CallbackActivity.kt deleted file mode 100644 index 5e52308..0000000 --- a/android/src/main/kotlin/com/linusu/flutter_web_auth/CallbackActivity.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.linusu.flutter_web_auth - -import android.app.Activity -import android.net.Uri -import android.os.Bundle - -class CallbackActivity: Activity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val url = intent?.data - val scheme = url?.scheme - - if (scheme != null) { - FlutterWebAuthPlugin.callbacks.remove(scheme)?.success(url.toString()) - } - - finish() - } -} diff --git a/android/src/main/kotlin/com/linusu/flutter_web_auth/FlutterWebAuthPlugin.kt b/android/src/main/kotlin/com/linusu/flutter_web_auth/FlutterWebAuthPlugin.kt index 520dcdd..4530703 100644 --- a/android/src/main/kotlin/com/linusu/flutter_web_auth/FlutterWebAuthPlugin.kt +++ b/android/src/main/kotlin/com/linusu/flutter_web_auth/FlutterWebAuthPlugin.kt @@ -1,74 +1,179 @@ package com.linusu.flutter_web_auth -import android.content.Context +import android.app.Activity +import android.app.Application import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri - +import android.os.Bundle +import android.text.TextUtils +import android.util.Log import androidx.browser.customtabs.CustomTabsIntent - +import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry.Registrar +import io.flutter.plugin.common.PluginRegistry + +class FlutterWebAuthPlugin( + private var channel: MethodChannel? = null, + private var activityBinding: ActivityPluginBinding? = null +) : MethodCallHandler, FlutterPlugin, ActivityAware, PluginRegistry.NewIntentListener { + + private lateinit var lifecycleListener: ActivityLifecycleListener + + private val activity: Activity + get() = activityBinding!!.activity -class FlutterWebAuthPlugin(private var context: Context? = null, private var channel: MethodChannel? = null): MethodCallHandler, FlutterPlugin { - companion object { - val callbacks = mutableMapOf() + companion object { + val callbacks = mutableMapOf() + } + + private fun initInstance(messenger: BinaryMessenger) { + channel = MethodChannel(messenger, "flutter_web_auth") + channel?.setMethodCallHandler(this) + } + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + initInstance(binding.binaryMessenger) + } - @JvmStatic - fun registerWith(registrar: Registrar) { - val plugin = FlutterWebAuthPlugin() - plugin.initInstance(registrar.messenger(), registrar.context()) + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activityBinding = binding.apply { + addOnNewIntentListener(this@FlutterWebAuthPlugin) + lifecycleListener = ActivityLifecycleListener(activity.javaClass.name) { + cleanUpDanglingCalls() + } + activity.application.registerActivityLifecycleCallbacks(lifecycleListener) + } } - } + override fun onDetachedFromActivityForConfigChanges() { + activityBinding?.apply { + removeOnNewIntentListener(this@FlutterWebAuthPlugin) + activity.application.unregisterActivityLifecycleCallbacks(lifecycleListener) + } + activityBinding = null + } - fun initInstance(messenger: BinaryMessenger, context: Context) { - this.context = context - channel = MethodChannel(messenger, "flutter_web_auth") - channel?.setMethodCallHandler(this) - } + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activityBinding = binding.apply { + addOnNewIntentListener(this@FlutterWebAuthPlugin) + activity.application.registerActivityLifecycleCallbacks(lifecycleListener) + } + } - override public fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - initInstance(binding.getBinaryMessenger(), binding.getApplicationContext()) - } + override fun onDetachedFromActivity() { + activityBinding?.apply { + removeOnNewIntentListener(this@FlutterWebAuthPlugin) + activity.application.unregisterActivityLifecycleCallbacks(lifecycleListener) + } + activityBinding = null + } - override public fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - context = null - channel = null - } - override fun onMethodCall(call: MethodCall, resultCallback: Result) { - when (call.method) { - "authenticate" -> { - val url = Uri.parse(call.argument("url")) - val callbackUrlScheme = call.argument("callbackUrlScheme")!! - val preferEphemeral = call.argument("preferEphemeral")!! + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel = null + } - callbacks[callbackUrlScheme] = resultCallback + override fun onMethodCall(call: MethodCall, resultCallback: Result) { + when (call.method) { + "authenticate" -> { + val url = Uri.parse(call.argument("url")) + val callbackUrlScheme = call.argument("callbackUrlScheme")!! + val preferEphemeral = call.argument("preferEphemeral")!! - val intent = CustomTabsIntent.Builder().build() - val keepAliveIntent = Intent(context, KeepAliveService::class.java) + callbacks[callbackUrlScheme] = resultCallback - intent.intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - if (preferEphemeral) { - intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - } - intent.intent.putExtra("android.support.customtabs.extra.KEEP_ALIVE", keepAliveIntent) + val intent = CustomTabsIntent.Builder().build() + val keepAliveIntent = Intent(activity, KeepAliveService::class.java) - intent.launchUrl(context, url) + intent.intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) + if (preferEphemeral) { + intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + } + intent.intent.putExtra( + "android.support.customtabs.extra.KEEP_ALIVE", + keepAliveIntent + ) + intent.intent.data = url + val CHROME_PACKAGE_NAME = "com.android.chrome" + val resolveInfoList = activity.packageManager.queryIntentActivities(intent + .intent, PackageManager.MATCH_ALL) + for (resolveInfo in resolveInfoList) { + val packageName = resolveInfo.activityInfo.packageName + if (TextUtils.equals(packageName, CHROME_PACKAGE_NAME)) + intent.intent.setPackage(CHROME_PACKAGE_NAME) + } + intent.launchUrl(activity, url) + } + else -> resultCallback.notImplemented() } - "cleanUpDanglingCalls" -> { - callbacks.forEach{ (_, danglingResultCallback) -> - danglingResultCallback.error("CANCELED", "User canceled login", null) - } - callbacks.clear() - resultCallback.success(null) + } + + private fun cleanUpDanglingCalls() { + if (callbacks.isNotEmpty()) { + callbacks.forEach { (_, danglingResultCallback) -> + danglingResultCallback.error("CANCELED", "User canceled login", null) + } + callbacks.clear() } - else -> resultCallback.notImplemented() } - } + + override fun onNewIntent(intent: Intent): Boolean { + if (intent.action == Intent.ACTION_VIEW && + intent.hasCategory(Intent.CATEGORY_BROWSABLE) + ) { + val url = intent.data + val scheme = url?.scheme + callbacks.remove(scheme)?.success(url?.toString()) + return true + } + return false + } } + +private class ActivityLifecycleListener( + val appActivityName: String, + val onReturnFromBrowser: () -> Unit +) : Application.ActivityLifecycleCallbacks { + + var paused: Boolean = false + + override fun onActivityPaused(activity: Activity) { + verifyActivity(activity) { + paused = true + } + } + + override fun onActivityStopped(activity: Activity) {} + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + + override fun onActivityDestroyed(activity: Activity) {} + + override fun onActivityResumed(activity: Activity) { + verifyActivity(activity) { + if (paused) { + Log.d("FlutterWebAuthPlugin", "onReturnFromBrowser") + onReturnFromBrowser() + paused = false + } + } + } + + fun verifyActivity(activity: Activity, success: () -> Unit) { + if (activity.javaClass.name == appActivityName && activity is FlutterActivity) { + success() + } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + + override fun onActivityStarted(activity: Activity) {} +} \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 1182eac..c9c3bb6 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 29 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -40,7 +40,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.linusu.flutter_web_auth_example" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -61,7 +61,7 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 113eb6c..47d8339 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,6 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> - - - - - - - - - - diff --git a/example/android/app/src/main/kotlin/com/linusu/flutter_web_auth_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/linusu/flutter_web_auth_example/MainActivity.kt index b1be57f..c7440bb 100644 --- a/example/android/app/src/main/kotlin/com/linusu/flutter_web_auth_example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/com/linusu/flutter_web_auth_example/MainActivity.kt @@ -1,13 +1,5 @@ package com.linusu.flutter_web_auth_example -import android.os.Bundle +import io.flutter.embedding.android.FlutterActivity -import io.flutter.app.FlutterActivity -import io.flutter.plugins.GeneratedPluginRegistrant - -class MainActivity: FlutterActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - GeneratedPluginRegistrant.registerWith(this) - } -} +class MainActivity: FlutterActivity() diff --git a/example/android/build.gradle b/example/android/build.gradle index 45d37ba..457e698 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.72' + ext.kotlin_version = '1.5.21' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + classpath 'com.android.tools.build:gradle:3.6.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle.properties b/example/android/gradle.properties index a673820..94adc3a 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -android.enableR8=true diff --git a/example/pubspec.lock b/example/pubspec.lock index 573b7d8..d4b70a6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" boolean_selector: dependency: transitive description: @@ -28,7 +28,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -87,7 +87,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: @@ -141,7 +141,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" typed_data: dependency: transitive description: diff --git a/ios/Classes/SwiftFlutterWebAuthPlugin.swift b/ios/Classes/SwiftFlutterWebAuthPlugin.swift index e4fe474..3d11576 100644 --- a/ios/Classes/SwiftFlutterWebAuthPlugin.swift +++ b/ios/Classes/SwiftFlutterWebAuthPlugin.swift @@ -3,6 +3,12 @@ import SafariServices import Flutter import UIKit +/// Conform your `AppDelegate` to the protocol +/// in order to get `ASWebAuthenticationSession` work +public protocol FlutterPresentationContextProviding: UIApplicationDelegate { + var flutterController: FlutterViewController? { get } +} + public class SwiftFlutterWebAuthPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "flutter_web_auth", binaryMessenger: registrar.messenger()) @@ -46,11 +52,13 @@ public class SwiftFlutterWebAuthPlugin: NSObject, FlutterPlugin { let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme, completionHandler: completionHandler) if #available(iOS 13, *) { - guard let provider = UIApplication.shared.delegate?.window??.rootViewController as? FlutterViewController else { + guard let appDelegate = UIApplication.shared.delegate as? FlutterPresentationContextProviding else { + // Will receive nil if AppDelegate doesn't conform + // to the `FlutterPresentationContextProviding` protocol result(FlutterError(code: "FAILED", message: "Failed to aquire root FlutterViewController" , details: nil)) return } - + let provider = appDelegate.flutterController session.prefersEphemeralWebBrowserSession = preferEphemeral session.presentationContextProvider = provider } @@ -64,9 +72,6 @@ public class SwiftFlutterWebAuthPlugin: NSObject, FlutterPlugin { } else { result(FlutterError(code: "FAILED", message: "This plugin does currently not support iOS lower than iOS 11" , details: nil)) } - } else if (call.method == "cleanUpDanglingCalls") { - // we do not keep track of old callbacks on iOS, so nothing to do here - result(nil) } else { result(FlutterMethodNotImplemented) } diff --git a/lib/flutter_web_auth.dart b/lib/flutter_web_auth.dart index a57099e..8a7ee70 100644 --- a/lib/flutter_web_auth.dart +++ b/lib/flutter_web_auth.dart @@ -1,27 +1,10 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart' show MethodChannel; -class _OnAppLifecycleResumeObserver extends WidgetsBindingObserver { - final Function onResumed; - - _OnAppLifecycleResumeObserver(this.onResumed); - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - onResumed(); - } - } -} - class FlutterWebAuth { static const MethodChannel _channel = const MethodChannel('flutter_web_auth'); - static final _OnAppLifecycleResumeObserver _resumedObserver = _OnAppLifecycleResumeObserver(() { - _cleanUpDanglingCalls(); // unawaited - }); /// Ask the user to authenticate to the specified web service. /// @@ -30,20 +13,10 @@ class FlutterWebAuth { /// [callbackUrlScheme] should be a string specifying the scheme of the url that the page will redirect to upon successful authentication. /// [preferEphemeral] if this is specified as `true`, an ephemeral web browser session will be used where possible (`FLAG_ACTIVITY_NO_HISTORY` on Android, `prefersEphemeralWebBrowserSession` on iOS/macOS) static Future authenticate({required String url, required String callbackUrlScheme, bool? preferEphemeral}) async { - WidgetsBinding.instance?.removeObserver(_resumedObserver); // safety measure so we never add this observer twice - WidgetsBinding.instance?.addObserver(_resumedObserver); return await _channel.invokeMethod('authenticate', { 'url': url, 'callbackUrlScheme': callbackUrlScheme, 'preferEphemeral': preferEphemeral ?? false, }) as String; } - - /// On Android, the plugin has to store the Result callbacks in order to pass the result back to the caller of - /// `authenticate`. But if that result never comes the callback will dangle around forever. This can be called to - /// terminate all `authenticate` calls with an error. - static Future _cleanUpDanglingCalls() async { - await _channel.invokeMethod('cleanUpDanglingCalls'); - WidgetsBinding.instance?.removeObserver(_resumedObserver); - } } diff --git a/pubspec.lock b/pubspec.lock index 9e82bb7..d4896e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" boolean_selector: dependency: transitive description: @@ -28,7 +28,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -73,7 +73,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: @@ -127,7 +127,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" typed_data: dependency: transitive description: