feat: Implement new sign in flow

This commit is contained in:
Christian Kußowski 2026-02-01 17:57:03 +01:00
commit 4d7f0295ca
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
19 changed files with 944 additions and 559 deletions

View file

@ -0,0 +1,72 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/sign_in/view_model/flows/sso_login.dart';
import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart';
import 'package:fluffychat/utils/localized_exception_extension.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';
void connectToHomeserverFlow(
PublicHomeserverData homeserverData,
BuildContext context,
void Function(AsyncSnapshot<bool>) setState,
bool signUp,
) async {
setState(AsyncSnapshot.waiting());
try {
final homeserverInput = homeserverData.name!;
var homeserver = Uri.parse(homeserverInput);
if (homeserver.scheme.isEmpty) {
homeserver = Uri.https(homeserverInput, '');
}
final l10n = L10n.of(context);
final client = await Matrix.of(context).getLoginClient();
final (_, _, loginFlows, _) = await client.checkHomeserver(homeserver);
final supportsSso = loginFlows.any((flow) => flow.type == 'm.login.sso');
if (!supportsSso) {
final regLink = homeserverData.regLink;
if (signUp && regLink != null) {
await launchUrlString(regLink);
}
final pathSegments = List.of(
GoRouter.of(context).routeInformationProvider.value.uri.pathSegments,
);
pathSegments.removeLast();
pathSegments.add('login');
context.go('/${pathSegments.join('/')}', extra: client);
setState(AsyncSnapshot.withData(ConnectionState.done, true));
return;
}
if (kIsWeb || PlatformInfos.isLinux) {
final consent = await showOkCancelAlertDialog(
context: context,
title: l10n.appWantsToUseForLogin(homeserverInput),
message: l10n.appWantsToUseForLoginDescription,
okLabel: l10n.continueText,
);
if (consent != OkCancelResult.ok) return;
}
await ssoLoginFlow(client, context, signUp);
setState(AsyncSnapshot.withData(ConnectionState.done, true));
} catch (e, s) {
setState(AsyncSnapshot.withError(ConnectionState.done, e, s));
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
e.toLocalizedString(context, ExceptionContext.checkHomeserver),
),
),
);
}
}

View file

@ -0,0 +1,24 @@
import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart';
int sortHomeservers(PublicHomeserverData a, PublicHomeserverData b) {
return _calcHomeserverScore(b).compareTo(_calcHomeserverScore(a));
}
int _calcHomeserverScore(PublicHomeserverData homeserver) {
var score = 0;
if (homeserver.description?.isNotEmpty == true) score++;
if (homeserver.homepage?.isNotEmpty == true) score++;
score += (homeserver.languages?.length ?? 0);
score += (homeserver.features?.length ?? 0);
score += (homeserver.onlineStatus ?? 0);
if (homeserver.ipv6 == true) score++;
if (homeserver.isp?.isNotEmpty == true) score++;
if (homeserver.privacy?.isNotEmpty == true) score++;
if (homeserver.rules?.isNotEmpty == true) score++;
if (homeserver.version?.isNotEmpty == true) score++;
if (homeserver.usingVanillaReg == true) score--;
if (homeserver.regLink != null) score--;
if (homeserver.regMethod != 'SSO') score--;
if (homeserver.regMethod == 'In-house Element') score--;
return score;
}

View file

@ -0,0 +1,49 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:matrix/matrix.dart';
import 'package:universal_html/html.dart' as html;
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart';
Future<void> ssoLoginFlow(
Client client,
BuildContext context,
bool signUp,
) async {
final redirectUrl = kIsWeb
? Uri.parse(
html.window.location.href,
).resolveUri(Uri(pathSegments: ['auth.html'])).toString()
: (PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS)
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
: 'http://localhost:3001//login';
final url = client.homeserver!.replace(
path: '/_matrix/client/v3/login/sso/redirect',
queryParameters: {
'redirectUrl': redirectUrl,
'action': signUp ? 'register' : 'login',
},
);
final urlScheme =
(PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS)
? Uri.parse(redirectUrl).scheme
: "http://localhost:3001";
final result = await FlutterWebAuth2.authenticate(
url: url.toString(),
callbackUrlScheme: urlScheme,
options: FlutterWebAuth2Options(useWebview: PlatformInfos.isMobile),
);
final token = Uri.parse(result).queryParameters['loginToken'];
if (token?.isEmpty ?? false) return;
await client.login(
LoginType.mLoginToken,
token: token,
initialDeviceDisplayName: PlatformInfos.clientName,
);
}

View file

@ -0,0 +1,79 @@
class PublicHomeserverData {
final String? name;
final String? clientDomain;
final String? homepage;
final String? isp;
final String? staffJur;
final String? rules;
final String? privacy;
final bool? usingVanillaReg;
final String? description;
final String? regMethod;
final String? regLink;
final String? software;
final String? version;
final bool? captcha;
final bool? email;
final List<String>? languages;
final List<String>? features;
final int? onlineStatus;
final String? serverDomain;
final int? verStatus;
final int? roomDirectory;
final bool? slidingSync;
final bool? ipv6;
PublicHomeserverData({
this.name,
this.clientDomain,
this.homepage,
this.isp,
this.staffJur,
this.rules,
this.privacy,
this.usingVanillaReg,
this.description,
this.regMethod,
this.regLink,
this.software,
this.version,
this.captcha,
this.email,
this.languages,
this.features,
this.onlineStatus,
this.serverDomain,
this.verStatus,
this.roomDirectory,
this.slidingSync,
this.ipv6,
});
factory PublicHomeserverData.fromJson(Map<String, dynamic> json) {
return PublicHomeserverData(
name: json['name'],
clientDomain: json['client_domain'],
homepage: json['homepage'],
isp: json['isp'],
staffJur: json['staff_jur'],
rules: json['rules'],
privacy: json['privacy'],
usingVanillaReg: json['using_vanilla_reg'],
description: json['description'],
regMethod: json['reg_method'],
regLink: json['reg_link'],
software: json['software'],
version: json['version'],
captcha: json['captcha'],
email: json['email'],
languages: List<String>.from(json['languages'] ?? []),
features: List<String>.from(json['features'] ?? []),
onlineStatus: json['online_status'],
serverDomain: json['server_domain'],
verStatus: json['ver_status'],
roomDirectory: json['room_directory'],
slidingSync: json['sliding_sync'],
ipv6: json['ipv6'],
);
}
}

View file

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart';
class SignInState {
final PublicHomeserverData? selectedHomeserver;
final AsyncSnapshot<List<PublicHomeserverData>> publicHomeservers;
final List<PublicHomeserverData> filteredPublicHomeservers;
final AsyncSnapshot<bool> loginLoading;
const SignInState({
this.selectedHomeserver,
this.publicHomeservers = const AsyncSnapshot.nothing(),
this.loginLoading = const AsyncSnapshot.nothing(),
this.filteredPublicHomeservers = const [],
});
SignInState copyWith({
PublicHomeserverData? selectedHomeserver,
AsyncSnapshot<List<PublicHomeserverData>>? publicHomeservers,
AsyncSnapshot<bool>? loginLoading,
List<PublicHomeserverData>? filteredPublicHomeservers,
}) {
return SignInState(
selectedHomeserver: selectedHomeserver ?? this.selectedHomeserver,
publicHomeservers: publicHomeservers ?? this.publicHomeservers,
loginLoading: loginLoading ?? this.loginLoading,
filteredPublicHomeservers:
filteredPublicHomeservers ?? this.filteredPublicHomeservers,
);
}
}

View file

@ -0,0 +1,112 @@
import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:collection/collection.dart';
import 'package:matrix/matrix_api_lite/utils/logs.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/pages/sign_in/view_model/flows/sort_homeservers.dart';
import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart';
import 'package:fluffychat/pages/sign_in/view_model/sign_in_state.dart';
import 'package:fluffychat/widgets/matrix.dart';
class SignInViewModel extends ValueNotifier<SignInState> {
final MatrixState matrixService;
final bool signUp;
final TextEditingController filterTextController = TextEditingController();
SignInViewModel(this.matrixService, {required this.signUp})
: super(SignInState()) {
refreshPublicHomeservers();
filterTextController.addListener(_filterHomeservers);
}
@override
void dispose() {
filterTextController.removeListener(_filterHomeservers);
super.dispose();
}
void _filterHomeservers() {
final filterText = filterTextController.text.trim().toLowerCase();
final filteredPublicHomeservers =
value.publicHomeservers.data
?.where(
(homeserver) =>
homeserver.name?.toLowerCase().contains(filterText) ?? false,
)
.toList() ??
[];
final splitted = filterText.split('.');
if (splitted.length >= 2 && !splitted.any((part) => part.isEmpty)) {
if (!filteredPublicHomeservers.any(
(homeserver) => homeserver.name == filterText,
)) {
filteredPublicHomeservers.add(PublicHomeserverData(name: filterText));
}
}
value = value.copyWith(
filteredPublicHomeservers: filteredPublicHomeservers,
);
}
void refreshPublicHomeservers() async {
value = value.copyWith(publicHomeservers: AsyncSnapshot.waiting());
final defaultHomeserverData = PublicHomeserverData(
name: AppSettings.defaultHomeserver.value,
);
try {
final client = await matrixService.getLoginClient();
final response = await client.httpClient.get(AppConfig.homeserverList);
final json = jsonDecode(response.body) as Map<String, dynamic>;
final homeserverJsonList = json['public_servers'] as List;
final publicHomeservers = homeserverJsonList
.map((json) => PublicHomeserverData.fromJson(json))
.toList();
if (signUp) {
publicHomeservers.removeWhere((server) {
return server.regMethod == null;
});
}
publicHomeservers.sort(sortHomeservers);
final defaultServer =
publicHomeservers.singleWhereOrNull(
(server) => server.name == AppSettings.defaultHomeserver.value,
) ??
defaultHomeserverData;
publicHomeservers.insert(0, defaultServer);
value = value.copyWith(
selectedHomeserver: value.selectedHomeserver ?? publicHomeservers.first,
publicHomeservers: AsyncSnapshot.withData(
ConnectionState.done,
publicHomeservers,
),
);
} catch (e, s) {
Logs().w('Unable to fetch public homeservers...', e, s);
value = value.copyWith(
selectedHomeserver: defaultHomeserverData,
publicHomeservers: AsyncSnapshot.withData(ConnectionState.done, [
defaultHomeserverData,
]),
);
}
_filterHomeservers();
}
void selectHomeserver(PublicHomeserverData? publicHomeserverData) {
value = value.copyWith(selectedHomeserver: publicHomeserverData);
}
void setLoginLoading(AsyncSnapshot<bool> loginLoading) {
value = value.copyWith(loginLoading: loginLoading);
}
}