2021-06-06 17:07:19 +02:00
|
|
|
import 'dart:async';
|
|
|
|
|
2022-04-29 07:26:01 +02:00
|
|
|
import 'package:flutter/foundation.dart';
|
2020-04-12 10:35:45 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2021-10-26 18:50:34 +02:00
|
|
|
|
2022-04-29 07:26:01 +02:00
|
|
|
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
2023-03-18 17:02:12 +01:00
|
|
|
import 'package:collection/collection.dart';
|
|
|
|
import 'package:file_picker/file_picker.dart';
|
2022-04-29 07:26:01 +02:00
|
|
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
2023-06-11 18:04:31 +02:00
|
|
|
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
2022-04-29 07:26:01 +02:00
|
|
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
|
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
2021-10-26 18:50:34 +02:00
|
|
|
import 'package:matrix/matrix.dart';
|
2023-06-11 18:04:31 +02:00
|
|
|
import 'package:universal_html/html.dart' as html;
|
2021-10-26 18:50:34 +02:00
|
|
|
import 'package:vrouter/vrouter.dart';
|
2021-06-06 17:07:19 +02:00
|
|
|
|
2021-10-26 18:50:34 +02:00
|
|
|
import 'package:fluffychat/config/app_config.dart';
|
2021-11-09 21:32:16 +01:00
|
|
|
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart';
|
2023-06-11 18:04:31 +02:00
|
|
|
import 'package:fluffychat/utils/platform_infos.dart';
|
2021-10-26 18:50:34 +02:00
|
|
|
import 'package:fluffychat/widgets/matrix.dart';
|
2021-11-09 21:32:16 +01:00
|
|
|
import '../../utils/localized_exception_extension.dart';
|
2020-04-12 10:35:45 +02:00
|
|
|
|
2022-04-29 07:26:01 +02:00
|
|
|
import 'package:fluffychat/utils/tor_stub.dart'
|
|
|
|
if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart';
|
|
|
|
|
2021-01-17 15:33:31 +01:00
|
|
|
class HomeserverPicker extends StatefulWidget {
|
2022-01-29 12:35:03 +01:00
|
|
|
const HomeserverPicker({Key? key}) : super(key: key);
|
2021-10-14 18:09:30 +02:00
|
|
|
|
2021-01-17 15:33:31 +01:00
|
|
|
@override
|
2021-04-09 16:28:26 +02:00
|
|
|
HomeserverPickerController createState() => HomeserverPickerController();
|
2021-01-17 15:33:31 +01:00
|
|
|
}
|
2020-04-12 10:35:45 +02:00
|
|
|
|
2021-04-09 16:28:26 +02:00
|
|
|
class HomeserverPickerController extends State<HomeserverPicker> {
|
|
|
|
bool isLoading = false;
|
2022-04-15 14:31:57 +02:00
|
|
|
final TextEditingController homeserverController = TextEditingController(
|
|
|
|
text: AppConfig.defaultHomeserver,
|
|
|
|
);
|
2022-05-25 15:53:49 +02:00
|
|
|
|
2023-06-11 18:04:31 +02:00
|
|
|
String? error;
|
2022-04-15 14:31:57 +02:00
|
|
|
|
2022-04-29 07:26:01 +02:00
|
|
|
bool isTorBrowser = false;
|
|
|
|
|
|
|
|
Future<void> _checkTorBrowser() async {
|
|
|
|
if (!kIsWeb) return;
|
|
|
|
|
|
|
|
Hive.openBox('test').then((value) => null).catchError(
|
|
|
|
(e, s) async {
|
|
|
|
await showOkAlertDialog(
|
2023-03-02 10:57:52 +01:00
|
|
|
context: context,
|
|
|
|
title: L10n.of(context)!.indexedDbErrorTitle,
|
|
|
|
message: L10n.of(context)!.indexedDbErrorLong,
|
|
|
|
onWillPop: () async => false,
|
|
|
|
);
|
2022-04-29 07:26:01 +02:00
|
|
|
_checkTorBrowser();
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
final isTor = await TorBrowserDetector.isTorBrowser;
|
2022-11-13 18:13:01 +01:00
|
|
|
isTorBrowser = isTor;
|
2022-04-29 07:26:01 +02:00
|
|
|
}
|
|
|
|
|
2023-06-11 18:04:31 +02:00
|
|
|
String? _lastCheckedUrl;
|
2021-11-16 10:23:29 +01:00
|
|
|
|
2021-04-09 16:28:26 +02:00
|
|
|
/// 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
|
2021-06-11 10:08:04 +02:00
|
|
|
/// login type.
|
2023-06-11 18:04:31 +02:00
|
|
|
Future<void> checkHomeserverAction([_]) async {
|
|
|
|
homeserverController.text =
|
|
|
|
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
|
|
|
|
if (homeserverController.text == _lastCheckedUrl) return;
|
|
|
|
_lastCheckedUrl = homeserverController.text;
|
2021-11-16 10:23:29 +01:00
|
|
|
setState(() {
|
2023-06-11 18:04:31 +02:00
|
|
|
error = _rawLoginTypes = loginHomeserverSummary = null;
|
2021-11-16 10:23:29 +01:00
|
|
|
isLoading = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
try {
|
2022-04-15 11:42:59 +02:00
|
|
|
var homeserver = Uri.parse(homeserverController.text);
|
|
|
|
if (homeserver.scheme.isEmpty) {
|
|
|
|
homeserver = Uri.https(homeserverController.text, '');
|
|
|
|
}
|
2023-06-11 18:04:31 +02:00
|
|
|
final client = Matrix.of(context).getLoginClient();
|
|
|
|
loginHomeserverSummary = await client.checkHomeserver(homeserver);
|
|
|
|
if (supportsSso) {
|
|
|
|
_rawLoginTypes = await client.request(
|
|
|
|
RequestType.GET,
|
|
|
|
'/client/r0/login',
|
|
|
|
);
|
2021-11-15 12:14:00 +01:00
|
|
|
}
|
2021-01-17 15:33:31 +01:00
|
|
|
} catch (e) {
|
2022-01-29 12:35:03 +01:00
|
|
|
setState(() => error = (e).toLocalizedString(context));
|
2021-01-17 15:33:31 +01:00
|
|
|
} finally {
|
|
|
|
if (mounted) {
|
2021-04-09 16:28:26 +02:00
|
|
|
setState(() => isLoading = false);
|
2021-01-17 15:33:31 +01:00
|
|
|
}
|
2020-04-12 10:35:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-11 18:04:31 +02:00
|
|
|
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<String, dynamic>? _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,
|
|
|
|
),
|
|
|
|
);
|
2022-04-15 14:31:57 +02:00
|
|
|
}
|
|
|
|
|
2023-06-11 18:04:31 +02:00
|
|
|
List<IdentityProvider>? 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() => VRouter.of(context).to('login');
|
|
|
|
|
2022-04-15 14:31:57 +02:00
|
|
|
@override
|
|
|
|
void initState() {
|
2022-04-29 07:26:01 +02:00
|
|
|
_checkTorBrowser();
|
2022-04-15 14:31:57 +02:00
|
|
|
super.initState();
|
2023-06-11 18:04:31 +02:00
|
|
|
WidgetsBinding.instance.addPostFrameCallback(checkHomeserverAction);
|
2022-04-15 14:31:57 +02:00
|
|
|
}
|
|
|
|
|
2020-04-12 10:35:45 +02:00
|
|
|
@override
|
2021-05-23 13:28:55 +02:00
|
|
|
Widget build(BuildContext context) {
|
|
|
|
Matrix.of(context).navigatorContext = context;
|
|
|
|
return HomeserverPickerView(this);
|
|
|
|
}
|
2022-04-29 07:26:01 +02:00
|
|
|
|
|
|
|
Future<void> restoreBackup() async {
|
2023-03-18 17:02:12 +01:00
|
|
|
final picked = await FilePicker.platform.pickFiles(withData: true);
|
|
|
|
final file = picked?.files.firstOrNull;
|
|
|
|
if (file == null) return;
|
2022-04-29 07:26:01 +02:00
|
|
|
await showFutureLoadingDialog(
|
2023-03-02 10:57:52 +01:00
|
|
|
context: context,
|
|
|
|
future: () async {
|
|
|
|
try {
|
|
|
|
final client = Matrix.of(context).getLoginClient();
|
2023-03-18 17:02:12 +01:00
|
|
|
await client.importDump(String.fromCharCodes(file.bytes!));
|
2023-03-02 10:57:52 +01:00
|
|
|
Matrix.of(context).initMatrix();
|
|
|
|
} catch (e, s) {
|
|
|
|
Logs().e('Future error:', e, s);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2022-04-29 07:26:01 +02:00
|
|
|
}
|
2020-04-12 10:35:45 +02:00
|
|
|
}
|
2023-06-11 18:04:31 +02:00
|
|
|
|
|
|
|
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'],
|
|
|
|
);
|
|
|
|
}
|