You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			239 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Dart
		
	
			
		
		
	
	
			239 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Dart
		
	
import 'dart:async';
 | 
						|
 | 
						|
import 'package:flutter/foundation.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
 | 
						|
import 'package:collection/collection.dart';
 | 
						|
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
 | 
						|
import 'package:go_router/go_router.dart';
 | 
						|
import 'package:matrix/matrix.dart';
 | 
						|
import 'package:universal_html/html.dart' as html;
 | 
						|
import 'package:url_launcher/url_launcher_string.dart';
 | 
						|
 | 
						|
import 'package:fluffychat/config/app_config.dart';
 | 
						|
import 'package:fluffychat/l10n/l10n.dart';
 | 
						|
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart';
 | 
						|
import 'package:fluffychat/utils/file_selector.dart';
 | 
						|
import 'package:fluffychat/utils/platform_infos.dart';
 | 
						|
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.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 {
 | 
						|
  final bool addMultiAccount;
 | 
						|
  const HomeserverPicker({required this.addMultiAccount, super.key});
 | 
						|
 | 
						|
  @override
 | 
						|
  HomeserverPickerController createState() => HomeserverPickerController();
 | 
						|
}
 | 
						|
 | 
						|
class HomeserverPickerController extends State<HomeserverPicker> {
 | 
						|
  bool isLoading = false;
 | 
						|
 | 
						|
  final TextEditingController homeserverController = TextEditingController(
 | 
						|
    text: AppConfig.defaultHomeserver,
 | 
						|
  );
 | 
						|
 | 
						|
  String? error;
 | 
						|
 | 
						|
  bool isTorBrowser = false;
 | 
						|
 | 
						|
  Future<void> _checkTorBrowser() async {
 | 
						|
    if (!kIsWeb) return;
 | 
						|
 | 
						|
    final isTor = await TorBrowserDetector.isTorBrowser;
 | 
						|
    isTorBrowser = isTor;
 | 
						|
  }
 | 
						|
 | 
						|
  /// 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<void> checkHomeserverAction({bool legacyPasswordLogin = false}) async {
 | 
						|
    final homeserverInput =
 | 
						|
        homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
 | 
						|
 | 
						|
    if (homeserverInput.isEmpty) {
 | 
						|
      final client = await Matrix.of(context).getLoginClient();
 | 
						|
      setState(() {
 | 
						|
        error = loginFlows = null;
 | 
						|
        isLoading = false;
 | 
						|
        client.homeserver = null;
 | 
						|
      });
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    setState(() {
 | 
						|
      error = loginFlows = null;
 | 
						|
      isLoading = true;
 | 
						|
    });
 | 
						|
 | 
						|
    final l10n = L10n.of(context);
 | 
						|
 | 
						|
    try {
 | 
						|
      var homeserver = Uri.parse(homeserverInput);
 | 
						|
      if (homeserver.scheme.isEmpty) {
 | 
						|
        homeserver = Uri.https(homeserverInput, '');
 | 
						|
      }
 | 
						|
      final client = await Matrix.of(context).getLoginClient();
 | 
						|
      final (_, _, loginFlows) = await client.checkHomeserver(homeserver);
 | 
						|
      this.loginFlows = loginFlows;
 | 
						|
      if (supportsSso && !legacyPasswordLogin) {
 | 
						|
        if (!PlatformInfos.isMobile) {
 | 
						|
          final consent = await showOkCancelAlertDialog(
 | 
						|
            context: context,
 | 
						|
            title: l10n.appWantsToUseForLogin(homeserverInput),
 | 
						|
            message: l10n.appWantsToUseForLoginDescription,
 | 
						|
            okLabel: l10n.continueText,
 | 
						|
          );
 | 
						|
          if (consent != OkCancelResult.ok) return;
 | 
						|
        }
 | 
						|
        return ssoLoginAction();
 | 
						|
      }
 | 
						|
      context.push(
 | 
						|
        '${GoRouter.of(context).routeInformationProvider.value.uri.path}/login',
 | 
						|
        extra: client,
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      setState(
 | 
						|
        () => error = (e).toLocalizedString(
 | 
						|
          context,
 | 
						|
          ExceptionContext.checkHomeserver,
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    } finally {
 | 
						|
      if (mounted) {
 | 
						|
        setState(() => isLoading = false);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  List<LoginFlow>? loginFlows;
 | 
						|
 | 
						|
  bool _supportsFlow(String flowType) =>
 | 
						|
      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');
 | 
						|
 | 
						|
  void ssoLoginAction() async {
 | 
						|
    final redirectUrl = kIsWeb
 | 
						|
        ? Uri.parse(html.window.location.href)
 | 
						|
            .resolveUri(
 | 
						|
              Uri(pathSegments: ['auth.html']),
 | 
						|
            )
 | 
						|
            .toString()
 | 
						|
        : isDefaultPlatform
 | 
						|
            ? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
 | 
						|
            : 'http://localhost:3001//login';
 | 
						|
    final client = await Matrix.of(context).getLoginClient();
 | 
						|
    final url = client.homeserver!.replace(
 | 
						|
      path: '/_matrix/client/v3/login/sso/redirect',
 | 
						|
      queryParameters: {'redirectUrl': redirectUrl},
 | 
						|
    );
 | 
						|
 | 
						|
    final urlScheme = isDefaultPlatform
 | 
						|
        ? Uri.parse(redirectUrl).scheme
 | 
						|
        : "http://localhost:3001";
 | 
						|
    final result = await FlutterWebAuth2.authenticate(
 | 
						|
      url: url.toString(),
 | 
						|
      callbackUrlScheme: urlScheme,
 | 
						|
      options: const FlutterWebAuth2Options(),
 | 
						|
    );
 | 
						|
    final token = Uri.parse(result).queryParameters['loginToken'];
 | 
						|
    if (token?.isEmpty ?? false) return;
 | 
						|
 | 
						|
    setState(() {
 | 
						|
      error = null;
 | 
						|
      isLoading = true;
 | 
						|
    });
 | 
						|
    try {
 | 
						|
      await client.login(
 | 
						|
        LoginType.mLoginToken,
 | 
						|
        token: token,
 | 
						|
        initialDeviceDisplayName: PlatformInfos.clientName,
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      setState(() {
 | 
						|
        error = e.toLocalizedString(context);
 | 
						|
      });
 | 
						|
    } finally {
 | 
						|
      if (mounted) {
 | 
						|
        setState(() {
 | 
						|
          isLoading = false;
 | 
						|
        });
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void initState() {
 | 
						|
    _checkTorBrowser();
 | 
						|
    super.initState();
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) => HomeserverPickerView(this);
 | 
						|
 | 
						|
  Future<void> restoreBackup() async {
 | 
						|
    final picked = await selectFiles(context);
 | 
						|
    final file = picked.firstOrNull;
 | 
						|
    if (file == null) return;
 | 
						|
    setState(() {
 | 
						|
      error = null;
 | 
						|
      isLoading = true;
 | 
						|
    });
 | 
						|
    try {
 | 
						|
      final client = await Matrix.of(context).getLoginClient();
 | 
						|
      await client.importDump(String.fromCharCodes(await file.readAsBytes()));
 | 
						|
      Matrix.of(context).initMatrix();
 | 
						|
    } catch (e) {
 | 
						|
      setState(() {
 | 
						|
        error = e.toLocalizedString(context);
 | 
						|
      });
 | 
						|
    } finally {
 | 
						|
      if (mounted) {
 | 
						|
        setState(() {
 | 
						|
          isLoading = false;
 | 
						|
        });
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void onMoreAction(MoreLoginActions action) {
 | 
						|
    switch (action) {
 | 
						|
      case MoreLoginActions.importBackup:
 | 
						|
        restoreBackup();
 | 
						|
      case MoreLoginActions.privacy:
 | 
						|
        launchUrlString(AppConfig.privacyUrl);
 | 
						|
      case MoreLoginActions.about:
 | 
						|
        PlatformInfos.showDialog(context);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
enum MoreLoginActions { importBackup, privacy, about }
 | 
						|
 | 
						|
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<String, dynamic> json) =>
 | 
						|
      IdentityProvider(
 | 
						|
        id: json['id'],
 | 
						|
        name: json['name'],
 | 
						|
        icon: json['icon'],
 | 
						|
        brand: json['brand'],
 | 
						|
      );
 | 
						|
}
 |