import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; class HomeserverPicker extends StatefulWidget { const HomeserverPicker({Key? key}) : super(key: key); @override HomeserverPickerController createState() => HomeserverPickerController(); } class HomeserverPickerController extends State { bool isLoading = false; final TextEditingController homeserverController = TextEditingController( text: AppConfig.defaultHomeserver, ); String? error; bool isTorBrowser = false; Future _checkTorBrowser() async { if (!kIsWeb) return; Hive.openBox('test').then((value) => null).catchError( (e, s) async { await showOkAlertDialog( context: context, title: L10n.of(context)!.indexedDbErrorTitle, message: L10n.of(context)!.indexedDbErrorLong, onWillPop: () async => false, ); _checkTorBrowser(); }, ); final isTor = await TorBrowserDetector.isTorBrowser; isTorBrowser = isTor; } String? _lastCheckedUrl; /// Starts an analysis of the given homeserver. It uses the current domain and /// makes sure that it is prefixed with https. Then it searches for the /// well-known information and forwards to the login page depending on the /// login type. Future checkHomeserverAction([_]) async { homeserverController.text = homeserverController.text.trim().toLowerCase().replaceAll(' ', '-'); if (homeserverController.text == _lastCheckedUrl) return; _lastCheckedUrl = homeserverController.text; setState(() { error = _rawLoginTypes = loginHomeserverSummary = null; isLoading = true; }); try { var homeserver = Uri.parse(homeserverController.text); if (homeserver.scheme.isEmpty) { homeserver = Uri.https(homeserverController.text, ''); } final client = Matrix.of(context).getLoginClient(); loginHomeserverSummary = await client.checkHomeserver(homeserver); if (supportsSso) { _rawLoginTypes = await client.request( RequestType.GET, '/client/r0/login', ); } } catch (e) { setState(() => error = (e).toLocalizedString(context)); } finally { if (mounted) { setState(() => isLoading = false); } } } HomeserverSummary? loginHomeserverSummary; bool _supportsFlow(String flowType) => loginHomeserverSummary?.loginFlows.any((flow) => flow.type == flowType) ?? false; bool get supportsSso => _supportsFlow('m.login.sso'); bool isDefaultPlatform = (PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS); bool get supportsPasswordLogin => _supportsFlow('m.login.password'); Map? _rawLoginTypes; void ssoLoginAction(String id) async { final redirectUrl = kIsWeb ? '${html.window.origin!}/web/auth.html' : isDefaultPlatform ? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login' : 'http://localhost:3001//login'; final url = '${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}'; final urlScheme = isDefaultPlatform ? Uri.parse(redirectUrl).scheme : "http://localhost:3001"; final result = await FlutterWebAuth2.authenticate( url: url, callbackUrlScheme: urlScheme, ); final token = Uri.parse(result).queryParameters['loginToken']; if (token?.isEmpty ?? false) return; await showFutureLoadingDialog( context: context, future: () => Matrix.of(context).getLoginClient().login( LoginType.mLoginToken, token: token, initialDeviceDisplayName: PlatformInfos.clientName, ), ); } List? get identityProviders { final loginTypes = _rawLoginTypes; if (loginTypes == null) return null; final rawProviders = loginTypes.tryGetList('flows')!.singleWhere( (flow) => flow['type'] == AuthenticationTypes.sso, )['identity_providers']; final list = (rawProviders as List) .map((json) => IdentityProvider.fromJson(json)) .toList(); if (PlatformInfos.isCupertinoStyle) { list.sort((a, b) => a.brand == 'apple' ? -1 : 1); } return list; } void login() => context.go('/home/login'); @override void initState() { _checkTorBrowser(); super.initState(); WidgetsBinding.instance.addPostFrameCallback(checkHomeserverAction); } @override Widget build(BuildContext context) { Matrix.of(context).navigatorContext = context; return HomeserverPickerView(this); } Future restoreBackup() async { final picked = await FilePicker.platform.pickFiles(withData: true); final file = picked?.files.firstOrNull; if (file == null) return; await showFutureLoadingDialog( context: context, future: () async { try { final client = Matrix.of(context).getLoginClient(); await client.importDump(String.fromCharCodes(file.bytes!)); Matrix.of(context).initMatrix(); } catch (e, s) { Logs().e('Future error:', e, s); } }, ); } } class IdentityProvider { final String? id; final String? name; final String? icon; final String? brand; IdentityProvider({this.id, this.name, this.icon, this.brand}); factory IdentityProvider.fromJson(Map json) => IdentityProvider( id: json['id'], name: json['name'], icon: json['icon'], brand: json['brand'], ); }