build: Android's built-in Kotlin, the three plugins applying KGP themselves patched in third_party/

AGP 9 compiles Kotlin itself (android.builtInKotlin=true in every app),
and warns that applying the Kotlin Gradle Plugin will soon fail.
flutter_web_auth_2 5.1.0, keyboard_height_plugin 0.3.0 and flutter_tts
4.2.5 still apply it unconditionally, so their pub releases are copied
into third_party/ with only their android/build.gradle changed, and
the root overrides them by path (third_party/README.md says when to
drop each). The servers' images copy third_party/, as pub refuses an
override whose path is missing. third_party/ isn't our own code: left
out of the analysis and the format check.

All four apps' APKs build without the warning. A Kotlin daemon's
session file that slipped into Train's android/ goes, and every app's
android/.gitignore keeps .kotlin/ out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-04 00:15:55 +01:00
commit 438ce20dbf
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
12 changed files with 8440 additions and 23 deletions

View file

@ -0,0 +1,96 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_web_auth_2/src/options.dart';
import 'package:flutter_web_auth_2/src/platform/platform_is.dart';
import 'package:flutter_web_auth_2_platform_interface/flutter_web_auth_2_platform_interface.dart';
export 'src/options.dart';
export 'src/unsupported.dart'
if (dart.library.io) 'src/linows.dart'
if (dart.library.js_interop) 'src/web.dart';
class _OnAppLifecycleResumeObserver extends WidgetsBindingObserver {
_OnAppLifecycleResumeObserver(this.onResumed);
final Future<void> Function() onResumed;
@override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
if (state == AppLifecycleState.resumed) {
await onResumed();
}
}
}
/// Provides all the functions you - as a user - should access.
class FlutterWebAuth2 {
static final RegExp _schemeRegExp = RegExp(r'^[a-z][a-z\d+.-]*$');
static FlutterWebAuth2Platform get _platform =>
FlutterWebAuth2Platform.instance;
static final _OnAppLifecycleResumeObserver _resumedObserver =
_OnAppLifecycleResumeObserver(_cleanUpDanglingCalls);
static void _assertCallbackScheme(String callbackUrlScheme) {
if ((PlatformIs.web || (!PlatformIs.windows && !PlatformIs.linux)) &&
!_schemeRegExp.hasMatch(callbackUrlScheme)) {
throw ArgumentError.value(
callbackUrlScheme,
'callbackUrlScheme',
'must be a valid URL scheme',
);
}
}
/// Ask the user to authenticate to the specified web service.
///
/// 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].
///
/// [callbackUrlScheme] should be a string specifying the scheme of the URL
/// that the page will redirect to upon successful authentication.
/// If it is `https`, you also need to specify
/// [FlutterWebAuth2Options.httpsHost] and [FlutterWebAuth2Options.httpsPath]
/// on Apple devices running iOS >= 17.4 or macOS >= 14.4. This allows for
/// easy integration of Universal links.
///
/// [options] can be used to specify either both general and
/// platform-specific settings.
static Future<String> authenticate({
required String url,
required String callbackUrlScheme,
FlutterWebAuth2Options options = const FlutterWebAuth2Options(),
}) async {
assert(
!(kIsWeb && options.debugOrigin != null && !kDebugMode),
'Do not use debugOrigin in production',
);
_assertCallbackScheme(callbackUrlScheme);
WidgetsBinding.instance.removeObserver(
_resumedObserver,
); // safety measure so we never add this observer twice
WidgetsBinding.instance.addObserver(_resumedObserver);
return _platform.authenticate(
url: url,
callbackUrlScheme: callbackUrlScheme,
options: options.toJson(),
);
}
/// The plugin may need to store the resulting 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<void> _cleanUpDanglingCalls() async {
await _platform.clearAllDanglingCalls();
WidgetsBinding.instance.removeObserver(_resumedObserver);
}
}

View file

@ -0,0 +1,44 @@
import 'dart:async';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:flutter_web_auth_2/src/server.dart';
import 'package:flutter_web_auth_2/src/webview.dart';
import 'package:flutter_web_auth_2_platform_interface/flutter_web_auth_2_platform_interface.dart';
/// Implements the plugin interface for Linux and Windows (Linows).
class FlutterWebAuth2LinowsPlugin extends FlutterWebAuth2Platform {
final FlutterWebAuth2Platform _webviewImpl = FlutterWebAuth2WebViewPlugin();
final FlutterWebAuth2Platform _serverImpl = FlutterWebAuth2ServerPlugin();
/// Registers the Linows super-implementation.
static void registerWith() {
FlutterWebAuth2Platform.instance = FlutterWebAuth2LinowsPlugin();
}
@override
Future<String> authenticate({
required String url,
required String callbackUrlScheme,
required Map<String, dynamic> options,
}) async {
final parsedOptions = FlutterWebAuth2Options.fromJson(options);
if (parsedOptions.useWebview) {
return _webviewImpl.authenticate(
url: url,
callbackUrlScheme: callbackUrlScheme,
options: options,
);
}
return _serverImpl.authenticate(
url: url,
callbackUrlScheme: callbackUrlScheme,
options: options,
);
}
@override
Future clearAllDanglingCalls() async {
await _serverImpl.clearAllDanglingCalls();
await _webviewImpl.clearAllDanglingCalls();
}
}

View file

@ -0,0 +1 @@
// MacOS just needs the MethodChannel implementation

View file

@ -0,0 +1,183 @@
/// Default intent flags for opening the custom tabs intent on Android.
const defaultIntentFlags = 0;
/// "Ephemeral" intent flags for opening the custom tabs intent on Android.
@Deprecated('Use [preferEphemeral]')
const ephemeralIntentFlags = defaultIntentFlags | 1 << 30;
/// Default HTML code that generates a nice callback page.
const _defaultLandingPage = '''
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Access Granted</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html, body { margin: 0; padding: 0; }
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
}
#text {
padding: 2em;
text-align: center;
font-size: 2rem;
}
</style>
<script>
setTimeout(function() {
window.close()
}, 1);
</script>
</head>
<body>
<main>
<div id="text">This page should automatically close.
You may also close this page manually now.</div>
</main>
</body>
</html>
''';
/// Provides configuration options for calls to `authenticate`.
class FlutterWebAuth2Options {
/// Construct an instance and specify the wanted options.
const FlutterWebAuth2Options({
bool? preferEphemeral,
this.debugOrigin,
int? intentFlags,
this.windowName,
int? timeout,
String? landingPageHtml,
bool? silentAuth,
bool? useWebview,
this.httpsHost,
this.httpsPath,
this.customTabsPackageOrder,
}) : preferEphemeral = preferEphemeral ?? false,
intentFlags = intentFlags ?? defaultIntentFlags,
timeout = timeout ?? 5 * 60,
landingPageHtml = landingPageHtml ?? _defaultLandingPage,
silentAuth = silentAuth ?? false,
useWebview = useWebview ?? true;
/// Construct an instance from JSON format.
FlutterWebAuth2Options.fromJson(Map<String, dynamic> json)
: this(
preferEphemeral: json['preferEphemeral'],
debugOrigin: json['debugOrigin'],
intentFlags: json['intentFlags'],
windowName: json['windowName'],
timeout: json['timeout'],
landingPageHtml: json['landingPageHtml'],
silentAuth: json['silentAuth'],
useWebview: json['useWebview'],
httpsHost: json['httpsHost'],
httpsPath: json['httpsPath'],
customTabsPackageOrder: json['customTabsPackageOrder'],
);
/// **Only has an effect on iOS, Android and macOS!**
/// If this is `true`, an ephemeral web browser session
/// will be used where possible.
final bool preferEphemeral;
/// **Only has an effect on Web!**
/// Can be used to override the origin of the redirect URL.
/// This is useful for cases where the redirect URL is not on the same
/// domain (e.g. local testing).
final String? debugOrigin;
/// **Only has an effect on Android!**
/// Can be used to configure the intent flags for the custom tabs intent.
/// Possible values can be found
/// [here](https://developer.android.com/reference/android/content/Intent#setFlags(int))
/// or by using the flags from the `Flag` class from
/// [android_intent_plus](https://pub.dev/packages/android_intent_plus).
final int intentFlags;
/// **Only has an effect on Web!**
/// Can be used to pass a window name for the URL open call.
/// See [here](https://www.w3schools.com/jsref/met_win_open.asp) for
/// possible parameter values.
final String? windowName;
/// **Only has an effect on Linux, Web, and Windows!**
/// Can be used to specify a timeout in seconds when the authentication shall
/// be deemed unsuccessful. An error will be thrown in order to abort the
/// authentication process.
final int timeout;
/// **Only has an effect on Linux and Windows with [useWebview] = `false`!**
/// Can be used to customise the landing page which tells the user that the
/// authentication was successful. It is the literal HTML source code which
/// will be displayed using a `HttpServer`.
final String landingPageHtml;
/// **Only has an effect on Web!**
/// When set to `true`, the authentication URL will be loaded in a hidden
/// `iframe` instead of opening a new window or tab. This is primarily used
/// for silent authentication processes where a full-page redirect is not
/// desirable. It allows for a seamless user experience by performing the
/// authentication in the background. This approach is useful for token
/// refreshes or for maintaining user sessions without explicit user
/// interaction. IT IS YOUR RESPONSIBILITY TO MAKE SURE THAT THE URL IS SANE
/// AND DOES NOT CAUSE ANY HARM. IFRAMES CAN BE EXPLOITED IN MANY WAYS BY
/// MALICIOUS ATTACKERS!
final bool silentAuth;
/// **Only has an effect on Linux and Windows!**
/// When set to `true`, use the new Webview implementation.
/// When set to `false`, the old approach using an internal server is being
/// used in order to fetch the HTTP result. When using the internal server,
/// please keep in mind that you cannot choose any callback URL scheme, as
/// described in https://github.com/ThexXTURBOXx/flutter_web_auth_2/issues/25
final bool useWebview;
/// **Only has an effect on Android, iOS, Linux, macOS, and Windows!**
/// String specifying the **host** of the URL that the page will redirect to
/// upon successful authentication (callback URL).
/// When `callbackUrlScheme` is `https`, this **must** be specified on
/// Android and Apple devices running iOS >= 17.4 or macOS >= 14.4.
/// On Linux and Windows, this is used when `useWebview == true` in order to
/// compare with the host of the callback URL (no matter which scheme!).
final String? httpsHost;
/// **Only has an effect on Android, iOS, Linux, macOS, and Windows!**
/// String specifying the **path** of the URL that the page will redirect to
/// upon successful authentication (callback URL).
/// When `callbackUrlScheme` is `https`, this **must** be specified on
/// Android and Apple devices running iOS >= 17.4 or macOS >= 14.4.
/// On Linux and Windows, this is used when `useWebview == true` in order to
/// compare with the path of the callback URL (no matter which scheme!).
final String? httpsPath;
/// **Only has an effect on Android!**
/// Sets the Android browser priority for opening custom tabs.
/// Needs to be a list of packages providing a custom tabs
/// service. If a browser is not installed, the next on the list
/// is tested etc.
final List<String>? customTabsPackageOrder;
/// Convert this instance to JSON format.
Map<String, dynamic> toJson() => {
'preferEphemeral': preferEphemeral,
'debugOrigin': debugOrigin,
'intentFlags': intentFlags,
'windowName': windowName,
'timeout': timeout,
'landingPageHtml': landingPageHtml,
'silentAuth': silentAuth,
'useWebview': useWebview,
'customTabsPackageOrder': customTabsPackageOrder,
'httpsHost': httpsHost,
'httpsPath': httpsPath,
};
}

View file

@ -0,0 +1,73 @@
// Adapted from https://gist.github.com/rydmike/1771fe24c050ebfe792fa309371154d8
import 'package:flutter_web_auth_2/src/platform/universal_platform_none.dart'
if (dart.library.io) 'package:flutter_web_auth_2/src/platform/universal_platform_vm.dart'
if (dart.library.js_interop) 'package:flutter_web_auth_2/src/platform/universal_platform_web.dart';
/// A universal platform checker.
///
/// Can be used to check active physical Flutter platform on all platforms.
///
/// To check what host platform the app is running on use:
///
/// * PlatformIs.android
/// * PlatformIs.iOS
/// * PlatformIs.macOS
/// * PlatformIs.windows
/// * PlatformIs.linux
/// * PlatformIs.fuchsia
///
/// To check the device type use:
///
/// * PlatformIs.mobile (Android or iOS)
/// * PlatformIs.desktop (Windows, macOS or Linux)
///
/// Currently Fuchsia is not considered mobile nor desktop, even if it
/// might be so in the future.
///
/// To check if the Flutter application is running on Web you can use:
///
/// * PlatformIs.web
///
/// Alternatively the Flutter foundation compile time constant kIsWeb also
/// works well for that.
///
/// The platform checks are supported independently on web. You can use
/// PlatformIs windows, iOS, macOS, Android and Linux to check what the host
/// platform is when you are running a Flutter Web application.
///
/// Checking if we are running on a Fuchsia host in a Web browser, is not yet
/// fully supported. If running in a Web browser on Fuchsia, PlatformIs.web
/// will be true, but PlatformIs.fuchsia will be false. Future versions, when
/// Fuchsia is released, may fix this.
class PlatformIs {
PlatformIs._();
/// Is Web?
static bool get web => UniversalPlatform.web;
/// Is macOS?
static bool get macOS => UniversalPlatform.macOS;
/// Is Windows?
static bool get windows => UniversalPlatform.windows;
/// Is Linux?
static bool get linux => UniversalPlatform.linux;
/// Is Android?
static bool get android => UniversalPlatform.android;
/// Is iOS?
static bool get iOS => UniversalPlatform.iOS;
/// Is Fuchsia?
static bool get fuchsia => UniversalPlatform.fuchsia;
/// Is Mobile?
static bool get mobile => PlatformIs.iOS || PlatformIs.android;
/// Is Desktop?
static bool get desktop =>
PlatformIs.macOS || PlatformIs.windows || PlatformIs.linux;
}

View file

@ -0,0 +1,33 @@
// Adapted from https://gist.github.com/rydmike/1771fe24c050ebfe792fa309371154d8
// NOTE:
// Never import this library directly in the application. The PlatformIs
// class and library uses conditional imports to only import this file on
// VM platform builds.
/// UniversalPlatform for just a "none" implementation when the platform
/// cannot be identified for some reason.
class UniversalPlatform {
UniversalPlatform._();
/// Is Web?
static bool get web => false;
/// Is macOS?
static bool get macOS => false;
/// Is Windows?
static bool get windows => false;
/// Is Linux?
static bool get linux => false;
/// Is Android?
static bool get android => false;
/// Is iOS?
static bool get iOS => false;
/// Is Fuchsia?
static bool get fuchsia => false;
}

View file

@ -0,0 +1,37 @@
// Adapted from https://gist.github.com/rydmike/1771fe24c050ebfe792fa309371154d8
import 'dart:io';
// NOTE:
// Never import this library directly in the application. The PlatformIs
// class and library uses conditional imports to only import this file on
// VM platform builds.
/// UniversalPlatform for Flutter VM builds.
///
/// We are using Dart VM builds, so we use dart:io Platform to
/// get the current platform.
class UniversalPlatform {
UniversalPlatform._();
/// Is Web?
static bool get web => false;
/// Is macOS?
static bool get macOS => Platform.isMacOS;
/// Is Windows?
static bool get windows => Platform.isWindows;
/// Is Linux?
static bool get linux => Platform.isLinux;
/// Is Android?
static bool get android => Platform.isAndroid;
/// Is iOS?
static bool get iOS => Platform.isIOS;
/// Is Fuchsia?
static bool get fuchsia => Platform.isFuchsia;
}

View file

@ -0,0 +1,57 @@
// Adapted from https://gist.github.com/rydmike/1771fe24c050ebfe792fa309371154d8
// ignore: avoid_web_libraries_in_flutter
import 'package:web/web.dart';
// NOTE:
// Never import this library directly in the application. The PlatformIs
// class and library uses conditional imports to only import this file on
// Web platform builds.
final Navigator _nav = window.navigator;
/// UniversalPlatform for Flutter WEB build.
///
/// We can use dart:html Navigator to get the current platform.
///
/// This function is borrowed, with minor modifications, from GetX utils library
/// with MIT license.
/// Credits for it belong to its author Jonny Borges https://github.com/jonataslaw
/// https://github.com/jonataslaw/getx/blob/master/lib/get_utils/src/platform/platform_web.dart
///
class UniversalPlatform {
UniversalPlatform._();
/// Is Web?
static bool get web => true;
/// Is macOS?
static bool get macOS => _nav.appVersion.contains('Mac OS') && !iOS;
/// Is Windows?
static bool get windows => _nav.appVersion.contains('Win');
/// Is Linux?
static bool get linux =>
(_nav.appVersion.contains('Linux') || _nav.appVersion.contains('x11')) &&
!android;
/// Is Android?
/// Source: https://developer.chrome.com/multidevice/user-agent
static bool get android => _nav.appVersion.contains('Android ');
/// Is iOS?
/// maxTouchPoints is needed to separate iPad iOS13 vs new MacOS
static bool get iOS =>
_hasMatch(_nav.platform, '/iPad|iPhone|iPod/') ||
(_nav.platform == 'MacIntel' && _nav.maxTouchPoints > 1);
/// Is Fuchsia?
/// Theoretically we could be in a Web browser on Fuchsia too, but
/// we have no info on how to get that info yet, so we return false.
static bool get fuchsia => false;
}
bool _hasMatch(String? value, String pattern) =>
// ignore: avoid_bool_literals_in_conditional_expressions
(value == null) ? false : RegExp(pattern).hasMatch(value);

View file

@ -0,0 +1,71 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:flutter_web_auth_2_platform_interface/flutter_web_auth_2_platform_interface.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_to_front/window_to_front.dart';
/// Implements the plugin interface using an internal server (currently used by
/// Windows and Linux).
class FlutterWebAuth2ServerPlugin extends FlutterWebAuth2Platform {
HttpServer? _server;
Timer? _authTimeout;
@override
Future<String> authenticate({
required String url,
required String callbackUrlScheme,
required Map<String, dynamic> options,
}) async {
final parsedOptions = FlutterWebAuth2Options.fromJson(options);
// Validate callback url
final callbackUri = Uri.parse(callbackUrlScheme);
if (callbackUri.scheme != 'http' ||
(callbackUri.host != 'localhost' && callbackUri.host != '127.0.0.1') ||
!callbackUri.hasPort) {
throw ArgumentError(
'Callback url scheme must start with http://localhost:{port}',
);
}
await _server?.close(force: true);
_server = await HttpServer.bind('127.0.0.1', callbackUri.port);
String? result;
_authTimeout?.cancel();
_authTimeout = Timer(Duration(seconds: parsedOptions.timeout), () {
_server?.close();
});
await launchUrl(Uri.parse(url));
await _server!.listen((req) async {
req.response.headers.add('Content-Type', 'text/html');
req.response.write(parsedOptions.landingPageHtml);
await req.response.close();
result = req.requestedUri.toString();
await _server?.close();
_server = null;
}).asFuture();
await _server?.close(force: true);
_authTimeout?.cancel();
if (result != null) {
await WindowToFront.activate();
return result!;
}
throw PlatformException(message: 'User canceled login', code: 'CANCELED');
}
@override
Future clearAllDanglingCalls() async {
await _server?.close(force: true);
}
}

View file

@ -0,0 +1,31 @@
import 'package:flutter/services.dart';
import 'package:flutter_web_auth_2_platform_interface/flutter_web_auth_2_platform_interface.dart';
/// Implements the plugin interface for unsupported platforms (just throws
/// errors).
class FlutterWebAuth2UnsupportedPlugin extends FlutterWebAuth2Platform {
/// Registers the unsupported implementation.
static void registerWith() {
FlutterWebAuth2Platform.instance = FlutterWebAuth2UnsupportedPlugin();
}
@override
Future<String> authenticate({
required String url,
required String callbackUrlScheme,
required Map<String, dynamic> options,
}) async {
throw PlatformException(
message: 'Platform either unsupported or unrecognised.',
code: 'UNSUPPORTED',
);
}
@override
Future clearAllDanglingCalls() async {
throw PlatformException(
message: 'Platform either unsupported or unrecognised.',
code: 'UNSUPPORTED',
);
}
}

View file

@ -0,0 +1,183 @@
import 'dart:async';
import 'dart:convert';
import 'dart:js_interop';
import 'package:flutter/services.dart';
import 'package:flutter_web_auth_2/src/options.dart';
import 'package:flutter_web_auth_2_platform_interface/flutter_web_auth_2_platform_interface.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:web/web.dart';
/// Implements the plugin interface using iframes (for silent authentication)
/// and through an event listener system (for usual authentication).
class FlutterWebAuth2WebPlugin extends FlutterWebAuth2Platform {
/// Registers the web implementation.
static void registerWith(Registrar registrar) {
final channel = MethodChannel(
'flutter_web_auth_2',
const StandardMethodCodec(),
registrar,
);
final instance = FlutterWebAuth2WebPlugin();
channel.setMethodCallHandler(instance._handleMethodCall);
FlutterWebAuth2Platform.instance = instance;
}
Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'authenticate':
final url = call.arguments['url'].toString();
return authenticate(
url: url,
callbackUrlScheme: '',
options: call.arguments['options'],
);
case 'cleanUpDanglingCalls':
return clearAllDanglingCalls();
default:
throw PlatformException(
code: 'Unimplemented',
details: "The flutter_web_auth_2 plugin for web doesn't implement "
"the method '${call.method}'",
);
}
}
@override
Future<String> authenticate({
required String url,
required String callbackUrlScheme,
required Map<String, dynamic> options,
}) async {
final parsedOptions = FlutterWebAuth2Options.fromJson(options);
if (parsedOptions.silentAuth) {
final authIframe = (document.createElement('iframe') as HTMLIFrameElement)
..src = url
..style.display = 'none';
document.body?.append(authIframe);
final completer = Completer<String>();
StreamSubscription? messageSubscription;
Timer? responseTimeoutTimer;
messageSubscription = window.onMessage.listen((messageEvent) {
if (messageEvent.origin ==
(parsedOptions.debugOrigin ?? Uri.base.origin)) {
final data = messageEvent.data.dartify();
final authMessage =
data != null && data is Map ? data['flutter-web-auth-2'] : null;
if (authMessage is String) {
authIframe.remove();
completer.complete(authMessage);
responseTimeoutTimer?.cancel();
messageSubscription?.cancel();
}
}
});
// Add a timeout for the iframe response
responseTimeoutTimer =
Timer(Duration(seconds: parsedOptions.timeout), () {
authIframe.remove();
messageSubscription?.cancel();
completer.completeError(
PlatformException(
code: 'timeout',
message: 'Timeout waiting for the iframe response',
),
);
});
return completer.future;
}
await launchUrl(
Uri.parse(url),
webOnlyWindowName: parsedOptions.windowName,
);
// Remove the old record if it exists
const storageKey = 'flutter-web-auth-2';
window.localStorage.removeItem(storageKey);
Timer? lsTimer;
StreamSubscription? messageSubscription;
final completer = Completer<String>();
try {
// Periodically check for the callback value in local storage.
// If it exists, return it. If not, check the timeout.
// If the timeout has passed, throw an exception.
lsTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
final flutterWebAuthMessage = window.localStorage.getItem(storageKey);
if (flutterWebAuthMessage != null) {
completer.complete(flutterWebAuthMessage);
window.localStorage.removeItem(storageKey);
messageSubscription?.cancel();
timer.cancel();
} else if (timer.tick >= parsedOptions.timeout) {
messageSubscription?.cancel();
timer.cancel();
completer.completeError(
PlatformException(
code: 'error',
message: 'Timeout waiting for callback value',
),
);
}
});
// Traditional window.opener method of listening for the redirect
messageSubscription = window.onMessage.listen(
(messageEvent) {
if (messageEvent.origin ==
(parsedOptions.debugOrigin ?? Uri.base.origin)) {
final data = messageEvent.data.dartify();
final flutterWebAuthMessage =
data != null && data is Map ? data['flutter-web-auth-2'] : null;
if (flutterWebAuthMessage is String) {
lsTimer?.cancel();
messageSubscription?.cancel();
completer.complete(flutterWebAuthMessage);
}
}
final appleOrigin = Uri(scheme: 'https', host: 'appleid.apple.com');
if (messageEvent.origin == appleOrigin.toString()) {
try {
final data = jsonDecode(messageEvent.data.toString());
if (data['method'] == 'oauthDone') {
final appleAuth =
data['data']['authorization'] as Map<String, dynamic>?;
if (appleAuth != null) {
final appleAuthQuery = Uri(queryParameters: appleAuth).query;
lsTimer?.cancel();
messageSubscription?.cancel();
completer.complete(
appleOrigin.replace(fragment: appleAuthQuery).toString(),
);
}
}
} on FormatException {
// ignore exception
}
}
},
onError: (error) {
lsTimer?.cancel();
messageSubscription?.cancel();
throw error;
},
);
} catch (e) {
lsTimer?.cancel();
completer.completeError(e);
}
return completer.future;
}
@override
Future<void> clearAllDanglingCalls() async {}
}

View file

@ -0,0 +1,83 @@
import 'dart:async';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/services.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:flutter_web_auth_2_platform_interface/flutter_web_auth_2_platform_interface.dart';
import 'package:path_provider/path_provider.dart';
/// Implements the plugin interface using the Webview interface (currently used
/// by Windows and Linux).
class FlutterWebAuth2WebViewPlugin extends FlutterWebAuth2Platform {
bool _authenticated = false;
Webview? _webview;
@override
Future<String> authenticate({
required String url,
required String callbackUrlScheme,
required Map<String, dynamic> options,
}) async {
if (!await WebviewWindow.isWebviewAvailable()) {
// Microsoft's WebView2 must be installed for this to work
throw StateError('Webview is not available');
}
final parsedOptions = FlutterWebAuth2Options.fromJson(options);
// Reset
_authenticated = false;
_webview?.close();
final c = Completer<String>();
_webview = await WebviewWindow.create(
configuration: CreateConfiguration(
windowHeight: 720,
windowWidth: 1280,
title: 'Authenticate',
titleBarTopPadding: 0,
userDataFolderWindows: (await getTemporaryDirectory()).path,
),
);
_webview!.setOnUrlRequestCallback((url) {
final uri = Uri.parse(url);
if (uri.scheme != callbackUrlScheme ||
(parsedOptions.httpsHost != null &&
uri.host != parsedOptions.httpsHost) ||
(parsedOptions.httpsPath != null &&
uri.path != parsedOptions.httpsPath)) {
return true;
}
_authenticated = true;
_webview?.close();
/**
* Not setting the webview to null will cause a crash if the
* application tries to open another webview
*/
_webview = null;
c.complete(url);
return true;
});
unawaited(
_webview!.onClose.whenComplete(
() {
/**
* Not setting the webview to null will cause a crash if the
* application tries to open another webview
*/
_webview = null;
if (!_authenticated) {
c.completeError(
PlatformException(code: 'CANCELED', message: 'User canceled'),
);
}
},
),
);
_webview!.launch(url);
return c.future;
}
@override
Future<void> clearAllDanglingCalls() async {}
}