mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-16 00:49:31 +01:00
feat: New simplified login process with more prominent SSO and nicer layout
This commit is contained in:
parent
db66793d28
commit
842ecc4235
Binary file not shown.
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 44 KiB |
@ -2473,5 +2473,8 @@
|
||||
"jump": "Jump",
|
||||
"openLinkInBrowser": "Open link in browser",
|
||||
"reportErrorDescription": "Oh no. Something went wrong. Please try again later. If you want, you can report the bug to the developers.",
|
||||
"report": "report"
|
||||
"report": "report",
|
||||
"signInWithPassword": "Sign in with password",
|
||||
"continueWith": "Continue with:",
|
||||
"pleaseTryAgainLaterOrChooseDifferentServer": "Please try again later or choose a different server."
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 146 KiB |
@ -9,7 +9,6 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart';
|
||||
import 'package:fluffychat/pages/connect/connect_page.dart';
|
||||
import 'package:fluffychat/pages/device_settings/device_settings.dart';
|
||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
|
||||
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
|
||||
@ -27,7 +26,6 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d
|
||||
import 'package:fluffychat/pages/settings_security/settings_security.dart';
|
||||
import 'package:fluffychat/pages/settings_stories/settings_stories.dart';
|
||||
import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
||||
import 'package:fluffychat/pages/sign_up/signup.dart';
|
||||
import 'package:fluffychat/pages/story/story_page.dart';
|
||||
import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
||||
import 'package:fluffychat/widgets/layouts/loading_view.dart';
|
||||
@ -266,23 +264,6 @@ class AppRoutes {
|
||||
widget: const Login(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: 'connect',
|
||||
widget: const ConnectPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
stackedRoutes: [
|
||||
VWidget(
|
||||
path: 'login',
|
||||
widget: const Login(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: 'signup',
|
||||
widget: const SignupPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
],
|
||||
),
|
||||
VWidget(
|
||||
path: 'logs',
|
||||
widget: const LogViewer(),
|
||||
@ -358,23 +339,6 @@ class AppRoutes {
|
||||
widget: const Login(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: 'connect',
|
||||
widget: const ConnectPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
stackedRoutes: [
|
||||
VWidget(
|
||||
path: 'login',
|
||||
widget: const Login(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: 'signup',
|
||||
widget: const SignupPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
VWidget(
|
||||
|
@ -1,197 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.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:image_picker/image_picker.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/connect/connect_page_view.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ConnectPage extends StatefulWidget {
|
||||
const ConnectPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ConnectPage> createState() => ConnectPageController();
|
||||
}
|
||||
|
||||
class ConnectPageController extends State<ConnectPage> {
|
||||
final TextEditingController usernameController = TextEditingController();
|
||||
String? signupError;
|
||||
bool loading = false;
|
||||
|
||||
void pickAvatar() async {
|
||||
final source = !PlatformInfos.isMobile
|
||||
? ImageSource.gallery
|
||||
: await showModalActionSheet<ImageSource>(
|
||||
context: context,
|
||||
title: L10n.of(context)!.changeYourAvatar,
|
||||
actions: [
|
||||
SheetAction(
|
||||
key: ImageSource.camera,
|
||||
label: L10n.of(context)!.openCamera,
|
||||
isDefaultAction: true,
|
||||
icon: Icons.camera_alt_outlined,
|
||||
),
|
||||
SheetAction(
|
||||
key: ImageSource.gallery,
|
||||
label: L10n.of(context)!.openGallery,
|
||||
icon: Icons.photo_outlined,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (source == null) return;
|
||||
final picked = await ImagePicker().pickImage(
|
||||
source: source,
|
||||
imageQuality: 50,
|
||||
maxWidth: 512,
|
||||
maxHeight: 512,
|
||||
);
|
||||
setState(() {
|
||||
Matrix.of(context).loginAvatar = picked;
|
||||
});
|
||||
}
|
||||
|
||||
void signUp() async {
|
||||
usernameController.text = usernameController.text.trim();
|
||||
final localpart =
|
||||
usernameController.text.toLowerCase().replaceAll(' ', '_');
|
||||
if (localpart.isEmpty) {
|
||||
setState(() {
|
||||
signupError = L10n.of(context)!.pleaseChooseAUsername;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
signupError = null;
|
||||
loading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
try {
|
||||
await Matrix.of(context).getLoginClient().register(username: localpart);
|
||||
} on MatrixException catch (e) {
|
||||
if (!e.requireAdditionalAuthentication) rethrow;
|
||||
}
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
Matrix.of(context).loginUsername = usernameController.text;
|
||||
VRouter.of(context).to('signup');
|
||||
} catch (e, s) {
|
||||
Logs().d('Sign up failed', e, s);
|
||||
setState(() {
|
||||
signupError = e.toLocalizedString(context);
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool _supportsFlow(String flowType) =>
|
||||
Matrix.of(context)
|
||||
.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 supportsLogin => _supportsFlow('m.login.password');
|
||||
|
||||
void login() => VRouter.of(context).to('login');
|
||||
|
||||
Map<String, dynamic>? _rawLoginTypes;
|
||||
|
||||
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 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (supportsSso) {
|
||||
Matrix.of(context)
|
||||
.getLoginClient()
|
||||
.request(
|
||||
RequestType.GET,
|
||||
'/client/r0/login',
|
||||
)
|
||||
.then(
|
||||
(loginTypes) => setState(() {
|
||||
_rawLoginTypes = loginTypes;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ConnectPageView(this);
|
||||
}
|
||||
|
||||
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'],
|
||||
);
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/connect/connect_page.dart';
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'sso_button.dart';
|
||||
|
||||
class ConnectPageView extends StatelessWidget {
|
||||
final ConnectPageController controller;
|
||||
const ConnectPageView(this.controller, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final avatar = Matrix.of(context).loginAvatar;
|
||||
final identityProviders = controller.identityProviders;
|
||||
return LoginScaffold(
|
||||
appBar: AppBar(
|
||||
leading: controller.loading ? null : const BackButton(),
|
||||
automaticallyImplyLeading: !controller.loading,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
Matrix.of(context).getLoginClient().homeserver?.host ?? '',
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
key: const Key('ConnectPageListView'),
|
||||
children: [
|
||||
if (Matrix.of(context).loginRegistrationSupported ?? false) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
Material(
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
elevation: Theme.of(context)
|
||||
.appBarTheme
|
||||
.scrolledUnderElevation ??
|
||||
10,
|
||||
color: Colors.transparent,
|
||||
shadowColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withAlpha(64),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: CircleAvatar(
|
||||
radius: 64,
|
||||
backgroundColor: Colors.white,
|
||||
child: avatar == null
|
||||
? const Icon(
|
||||
Icons.person,
|
||||
color: Colors.black,
|
||||
size: 64,
|
||||
)
|
||||
: FutureBuilder<Uint8List>(
|
||||
future: avatar.readAsBytes(),
|
||||
builder: (context, snapshot) {
|
||||
final bytes = snapshot.data;
|
||||
if (bytes == null) {
|
||||
return const CircularProgressIndicator
|
||||
.adaptive();
|
||||
}
|
||||
return Image.memory(
|
||||
bytes,
|
||||
fit: BoxFit.cover,
|
||||
width: 128,
|
||||
height: 128,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: FloatingActionButton(
|
||||
mini: true,
|
||||
onPressed: controller.pickAvatar,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
child: const Icon(Icons.camera_alt_outlined),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
controller: controller.usernameController,
|
||||
onSubmitted: (_) => controller.signUp(),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.account_box_outlined),
|
||||
hintText: L10n.of(context)!.chooseAUsername,
|
||||
errorText: controller.signupError,
|
||||
errorStyle: const TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Hero(
|
||||
tag: 'loginButton',
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: controller.loading ? () {} : controller.signUp,
|
||||
icon: const Icon(Icons.person_add_outlined),
|
||||
label: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.signUp),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Divider(
|
||||
thickness: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
L10n.of(context)!.or,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Divider(
|
||||
thickness: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (controller.supportsSso)
|
||||
identityProviders == null
|
||||
? const SizedBox(
|
||||
height: 74,
|
||||
child: Center(child: CircularProgressIndicator.adaptive()),
|
||||
)
|
||||
: Center(
|
||||
child: identityProviders.length == 1
|
||||
? Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer,
|
||||
foregroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
icon: identityProviders.single.icon == null
|
||||
? const Icon(
|
||||
Icons.web_outlined,
|
||||
size: 16,
|
||||
)
|
||||
: Image.network(
|
||||
Uri.parse(identityProviders.single.icon!)
|
||||
.getDownloadLink(
|
||||
Matrix.of(context).getLoginClient(),
|
||||
)
|
||||
.toString(),
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
onPressed: () => controller
|
||||
.ssoLoginAction(identityProviders.single.id!),
|
||||
label: Text(
|
||||
identityProviders.single.name ??
|
||||
identityProviders.single.brand ??
|
||||
L10n.of(context)!.loginWithOneClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Wrap(
|
||||
children: [
|
||||
for (final identityProvider in identityProviders)
|
||||
SsoButton(
|
||||
onPressed: () => controller
|
||||
.ssoLoginAction(identityProvider.id!),
|
||||
identityProvider: identityProvider,
|
||||
),
|
||||
].toList(),
|
||||
),
|
||||
),
|
||||
if (controller.supportsLogin)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Hero(
|
||||
tag: 'signinButton',
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.login_outlined),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: controller.loading ? () {} : controller.login,
|
||||
label: Text(L10n.of(context)!.login),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/connect/connect_page.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class SsoButton extends StatelessWidget {
|
||||
final IdentityProvider identityProvider;
|
||||
final void Function()? onPressed;
|
||||
const SsoButton({
|
||||
Key? key,
|
||||
required this.identityProvider,
|
||||
this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: identityProvider.icon == null
|
||||
? const Icon(Icons.web_outlined)
|
||||
: Image.network(
|
||||
Uri.parse(identityProvider.icon!)
|
||||
.getDownloadLink(
|
||||
Matrix.of(context).getLoginClient(),
|
||||
)
|
||||
.toString(),
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
identityProvider.name ??
|
||||
identityProvider.brand ??
|
||||
L10n.of(context)!.singlesignon,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'homeserver_bottom_sheet.dart';
|
||||
import 'homeserver_picker.dart';
|
||||
|
||||
class HomeserverAppBar extends StatelessWidget {
|
||||
@ -13,10 +16,42 @@ class HomeserverAppBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
focusNode: controller.homeserverFocusNode,
|
||||
return TypeAheadField<HomeserverBenchmarkResult>(
|
||||
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
elevation: Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4,
|
||||
shadowColor: Theme.of(context).appBarTheme.shadowColor ?? Colors.black,
|
||||
constraints: const BoxConstraints(maxHeight: 256),
|
||||
),
|
||||
itemBuilder: (context, homeserver) => ListTile(
|
||||
title: Text(homeserver.homeserver.baseUrl.toString()),
|
||||
subtitle: Text(homeserver.homeserver.description ?? ''),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.info_outlined),
|
||||
onPressed: () => showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (_) => HomeserverBottomSheet(
|
||||
homeserver: homeserver,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
suggestionsCallback: (pattern) async {
|
||||
final homeserverList =
|
||||
await const JoinmatrixOrgParser().fetchHomeservers();
|
||||
final benchmark = await HomeserverListProvider.benchmarkHomeserver(
|
||||
homeserverList,
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
return benchmark;
|
||||
},
|
||||
onSuggestionSelected: (suggestion) {
|
||||
controller.homeserverController.text =
|
||||
suggestion.homeserver.baseUrl.host;
|
||||
controller.checkHomeserverAction();
|
||||
},
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: controller.homeserverController,
|
||||
onChanged: controller.onChanged,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Navigator.of(context).canPop()
|
||||
? IconButton(
|
||||
@ -27,11 +62,11 @@ class HomeserverAppBar extends StatelessWidget {
|
||||
prefixText: '${L10n.of(context)!.homeserver}: ',
|
||||
hintText: L10n.of(context)!.enterYourHomeserver,
|
||||
suffixIcon: const Icon(Icons.search),
|
||||
errorText: controller.error,
|
||||
),
|
||||
readOnly: !AppConfig.allowOtherHomeservers,
|
||||
onSubmitted: (_) => controller.checkHomeserverAction(),
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: controller.checkHomeserverAction,
|
||||
autocorrect: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -7,16 +7,16 @@ 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:hive_flutter/hive_flutter.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_bottom_sheet.dart';
|
||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
|
||||
@ -35,14 +35,8 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
||||
final TextEditingController homeserverController = TextEditingController(
|
||||
text: AppConfig.defaultHomeserver,
|
||||
);
|
||||
final FocusNode homeserverFocusNode = FocusNode();
|
||||
String? error;
|
||||
List<HomeserverBenchmarkResult>? benchmarkResults;
|
||||
bool displayServerList = false;
|
||||
|
||||
bool get loadingHomeservers =>
|
||||
AppConfig.allowOtherHomeservers && benchmarkResults == null;
|
||||
String searchTerm = '';
|
||||
String? error;
|
||||
|
||||
bool isTorBrowser = false;
|
||||
|
||||
@ -65,98 +59,34 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
||||
isTorBrowser = isTor;
|
||||
}
|
||||
|
||||
void _updateFocus() {
|
||||
if (benchmarkResults == null) _loadHomeserverList();
|
||||
if (homeserverFocusNode.hasFocus) {
|
||||
setState(() {
|
||||
displayServerList = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void showServerInfo(HomeserverBenchmarkResult server) =>
|
||||
showAdaptiveBottomSheet(
|
||||
context: context,
|
||||
builder: (_) => HomeserverBottomSheet(
|
||||
homeserver: server,
|
||||
),
|
||||
);
|
||||
|
||||
void onChanged(String text) => setState(() {
|
||||
searchTerm = text;
|
||||
});
|
||||
|
||||
List<HomeserverBenchmarkResult> get filteredHomeservers => benchmarkResults!
|
||||
.where(
|
||||
(element) =>
|
||||
element.homeserver.baseUrl.host.contains(searchTerm) ||
|
||||
(element.homeserver.description?.contains(searchTerm) ?? false),
|
||||
)
|
||||
.toList();
|
||||
|
||||
void _loadHomeserverList() async {
|
||||
try {
|
||||
final homeserverList =
|
||||
await const JoinmatrixOrgParser().fetchHomeservers();
|
||||
final benchmark = await HomeserverListProvider.benchmarkHomeserver(
|
||||
homeserverList,
|
||||
timeout: const Duration(seconds: 10),
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
benchmarkResults = benchmark;
|
||||
});
|
||||
} catch (e, s) {
|
||||
Logs().e('Homeserver benchmark failed', e, s);
|
||||
benchmarkResults = [];
|
||||
}
|
||||
}
|
||||
|
||||
void setServer(String server) => setState(() {
|
||||
homeserverController.text = server;
|
||||
searchTerm = '';
|
||||
homeserverFocusNode.unfocus();
|
||||
displayServerList = false;
|
||||
});
|
||||
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<void> checkHomeserverAction() async {
|
||||
Future<void> checkHomeserverAction([_]) async {
|
||||
homeserverController.text =
|
||||
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
|
||||
if (homeserverController.text == _lastCheckedUrl) return;
|
||||
_lastCheckedUrl = homeserverController.text;
|
||||
setState(() {
|
||||
homeserverFocusNode.unfocus();
|
||||
error = null;
|
||||
error = _rawLoginTypes = loginHomeserverSummary = null;
|
||||
isLoading = true;
|
||||
searchTerm = '';
|
||||
displayServerList = false;
|
||||
});
|
||||
|
||||
try {
|
||||
homeserverController.text =
|
||||
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
|
||||
var homeserver = Uri.parse(homeserverController.text);
|
||||
if (homeserver.scheme.isEmpty) {
|
||||
homeserver = Uri.https(homeserverController.text, '');
|
||||
}
|
||||
final matrix = Matrix.of(context);
|
||||
matrix.loginHomeserverSummary =
|
||||
await matrix.getLoginClient().checkHomeserver(homeserver);
|
||||
final ssoSupported = matrix.loginHomeserverSummary!.loginFlows
|
||||
.any((flow) => flow.type == 'm.login.sso');
|
||||
|
||||
try {
|
||||
await Matrix.of(context).getLoginClient().register();
|
||||
matrix.loginRegistrationSupported = true;
|
||||
} on MatrixException catch (e) {
|
||||
matrix.loginRegistrationSupported = e.requireAdditionalAuthentication;
|
||||
}
|
||||
|
||||
if (!ssoSupported && matrix.loginRegistrationSupported == false) {
|
||||
// Server does not support SSO or registration. We can skip to login page:
|
||||
VRouter.of(context).to('login');
|
||||
} else {
|
||||
VRouter.of(context).to('connect');
|
||||
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));
|
||||
@ -167,17 +97,71 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
homeserverFocusNode.removeListener(_updateFocus);
|
||||
super.dispose();
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
homeserverFocusNode.addListener(_updateFocus);
|
||||
_checkTorBrowser();
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback(checkHomeserverAction);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -204,3 +188,20 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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'],
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import '../../config/themes.dart';
|
||||
import '../../widgets/mxc_image.dart';
|
||||
import 'homeserver_app_bar.dart';
|
||||
import 'homeserver_picker.dart';
|
||||
|
||||
@ -16,15 +16,19 @@ class HomeserverPickerView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final benchmarkResults = controller.benchmarkResults;
|
||||
final identityProviders = controller.identityProviders;
|
||||
final errorText = controller.error;
|
||||
return LoginScaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 12,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.all(0.0),
|
||||
child: HomeserverAppBar(controller: controller),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: HomeserverAppBar(controller: controller),
|
||||
),
|
||||
// display a prominent banner to import session for TOR browser
|
||||
// users. This feature is just some UX sugar as TOR users are
|
||||
// usually forced to logout as TOR browser is non-persistent
|
||||
@ -49,112 +53,153 @@ class HomeserverPickerView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: controller.displayServerList
|
||||
? ListView(
|
||||
child: controller.isLoading
|
||||
? const Center(child: CircularProgressIndicator.adaptive())
|
||||
: ListView(
|
||||
children: [
|
||||
if (controller.displayServerList)
|
||||
Image.asset(
|
||||
'assets/info-logo.png',
|
||||
height: 96,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Divider(
|
||||
thickness: 1,
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Material(
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: benchmarkResults == null
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: CircularProgressIndicator
|
||||
.adaptive(),
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children: controller.filteredHomeservers
|
||||
.map(
|
||||
(server) => ListTile(
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.info_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () => controller
|
||||
.showServerInfo(server),
|
||||
),
|
||||
onTap: () => controller.setServer(
|
||||
server.homeserver.baseUrl.host,
|
||||
),
|
||||
title: Text(
|
||||
server.homeserver.baseUrl.host,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
child: Text(
|
||||
L10n.of(context)!.continueWith,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
server.homeserver.description ??
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
Expanded(
|
||||
child: Divider(
|
||||
thickness: 1,
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Image.asset(
|
||||
'assets/banner_transparent.png',
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
),
|
||||
if (errorText != null) ...[
|
||||
const Center(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Text(
|
||||
errorText,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => launchUrlString(AppConfig.privacyUrl),
|
||||
child: Text(L10n.of(context)!.privacy),
|
||||
Center(
|
||||
child: Text(
|
||||
L10n.of(context)!
|
||||
.pleaseTryAgainLaterOrChooseDifferentServer,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
if (identityProviders != null) ...[
|
||||
...identityProviders.map(
|
||||
(provider) => _LoginButton(
|
||||
icon: provider.icon == null
|
||||
? const Icon(Icons.open_in_new_outlined)
|
||||
: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: MxcImage(
|
||||
placeholder: (_) =>
|
||||
const Icon(Icons.web_outlined),
|
||||
uri: Uri.parse(provider.icon!),
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
label: provider.name ??
|
||||
provider.brand ??
|
||||
L10n.of(context)!.singlesignon,
|
||||
onPressed: () =>
|
||||
controller.ssoLoginAction(provider.id!),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (controller.supportsPasswordLogin)
|
||||
_LoginButton(
|
||||
onPressed: controller.login,
|
||||
icon: const Icon(Icons.login_outlined),
|
||||
label: L10n.of(context)!.signInWithPassword,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: controller.restoreBackup,
|
||||
child: Text(L10n.of(context)!.hydrate),
|
||||
),
|
||||
Hero(
|
||||
tag: 'loginButton',
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: controller.isLoading
|
||||
? null
|
||||
: controller.checkHomeserverAction,
|
||||
icon: const Icon(Icons.start_outlined),
|
||||
label: controller.isLoading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.letsStart),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoginButton extends StatelessWidget {
|
||||
final Widget icon;
|
||||
final String label;
|
||||
final void Function() onPressed;
|
||||
|
||||
const _LoginButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
label: Text(label),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,129 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/pages/sign_up/signup_view.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
|
||||
class SignupPage extends StatefulWidget {
|
||||
const SignupPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
SignupPageController createState() => SignupPageController();
|
||||
}
|
||||
|
||||
class SignupPageController extends State<SignupPage> {
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
final TextEditingController password2Controller = TextEditingController();
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
String? error;
|
||||
bool loading = false;
|
||||
bool showPassword = false;
|
||||
bool noEmailWarningConfirmed = false;
|
||||
bool displaySecondPasswordField = false;
|
||||
|
||||
static const int minPassLength = 8;
|
||||
|
||||
void toggleShowPassword() => setState(() => showPassword = !showPassword);
|
||||
|
||||
String? get domain => VRouter.of(context).queryParameters['domain'];
|
||||
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
void onPasswordType(String text) {
|
||||
if (text.length >= minPassLength && !displaySecondPasswordField) {
|
||||
setState(() {
|
||||
displaySecondPasswordField = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String? password1TextFieldValidator(String? value) {
|
||||
if (value!.isEmpty) {
|
||||
return L10n.of(context)!.chooseAStrongPassword;
|
||||
}
|
||||
if (value.length < minPassLength) {
|
||||
return L10n.of(context)!
|
||||
.pleaseChooseAtLeastChars(minPassLength.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? password2TextFieldValidator(String? value) {
|
||||
if (value!.isEmpty) {
|
||||
return L10n.of(context)!.repeatPassword;
|
||||
}
|
||||
if (value != passwordController.text) {
|
||||
return L10n.of(context)!.passwordsDoNotMatch;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? emailTextFieldValidator(String? value) {
|
||||
if (value!.isEmpty && !noEmailWarningConfirmed) {
|
||||
noEmailWarningConfirmed = true;
|
||||
return L10n.of(context)!.noEmailWarning;
|
||||
}
|
||||
if (value.isNotEmpty && !value.contains('@')) {
|
||||
return L10n.of(context)!.pleaseEnterValidEmail;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void signup([_]) async {
|
||||
setState(() {
|
||||
error = null;
|
||||
});
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final client = Matrix.of(context).getLoginClient();
|
||||
final email = emailController.text;
|
||||
if (email.isNotEmpty) {
|
||||
Matrix.of(context).currentClientSecret =
|
||||
DateTime.now().millisecondsSinceEpoch.toString();
|
||||
Matrix.of(context).currentThreepidCreds =
|
||||
await client.requestTokenToRegisterEmail(
|
||||
Matrix.of(context).currentClientSecret,
|
||||
email,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
final displayname = Matrix.of(context).loginUsername!;
|
||||
final localPart = displayname.toLowerCase().replaceAll(' ', '_');
|
||||
|
||||
await client.uiaRequestBackground(
|
||||
(auth) => client.register(
|
||||
username: localPart,
|
||||
password: passwordController.text,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||
auth: auth,
|
||||
),
|
||||
);
|
||||
// Set displayname
|
||||
if (displayname != localPart) {
|
||||
await client.setDisplayName(
|
||||
client.userID!,
|
||||
displayname,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
error = (e).toLocalizedString(context);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SignupPageView(this);
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import 'signup.dart';
|
||||
|
||||
class SignupPageView extends StatelessWidget {
|
||||
final SignupPageController controller;
|
||||
const SignupPageView(this.controller, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LoginScaffold(
|
||||
appBar: AppBar(
|
||||
leading: controller.loading ? null : const BackButton(),
|
||||
automaticallyImplyLeading: !controller.loading,
|
||||
title: Text(L10n.of(context)!.signUp),
|
||||
),
|
||||
body: Form(
|
||||
key: controller.formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextFormField(
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
onChanged: controller.onPasswordType,
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.newPassword],
|
||||
controller: controller.passwordController,
|
||||
obscureText: !controller.showPassword,
|
||||
validator: controller.password1TextFieldValidator,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.vpn_key_outlined),
|
||||
suffixIcon: IconButton(
|
||||
tooltip: L10n.of(context)!.showPassword,
|
||||
icon: Icon(
|
||||
controller.showPassword
|
||||
? Icons.visibility_off_outlined
|
||||
: Icons.visibility_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: controller.toggleShowPassword,
|
||||
),
|
||||
errorStyle: const TextStyle(color: Colors.orange),
|
||||
hintText: L10n.of(context)!.chooseAStrongPassword,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.displaySecondPasswordField)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextFormField(
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.newPassword],
|
||||
controller: controller.password2Controller,
|
||||
obscureText: !controller.showPassword,
|
||||
validator: controller.password2TextFieldValidator,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.repeat_outlined),
|
||||
hintText: L10n.of(context)!.repeatPassword,
|
||||
errorStyle: const TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextFormField(
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
controller: controller.emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.username],
|
||||
validator: controller.emailTextFieldValidator,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.mail_outlined),
|
||||
hintText: L10n.of(context)!.enterAnEmailAddress,
|
||||
errorText: controller.error,
|
||||
errorMaxLines: 4,
|
||||
errorStyle: TextStyle(
|
||||
color: controller.emailController.text.isEmpty
|
||||
? Colors.orangeAccent
|
||||
: Colors.orange,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Hero(
|
||||
tag: 'loginButton',
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.person_add_outlined),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: controller.loading ? () {} : controller.signup,
|
||||
label: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.signUp),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
||||
class LoginScaffold extends StatelessWidget {
|
||||
final Widget body;
|
||||
@ -17,6 +21,7 @@ class LoginScaffold extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final isMobileMode = !FluffyThemes.isColumnMode(context);
|
||||
final scaffold = Scaffold(
|
||||
key: const Key('LoginScaffold'),
|
||||
backgroundColor: isMobileMode ? null : Colors.transparent,
|
||||
appBar: appBar == null
|
||||
? null
|
||||
@ -33,31 +38,102 @@ class LoginScaffold extends StatelessWidget {
|
||||
extendBodyBehindAppBar: true,
|
||||
extendBody: true,
|
||||
body: body,
|
||||
bottomNavigationBar: isMobileMode
|
||||
? Material(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
child: const _PrivacyButtons(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
if (isMobileMode) return scaffold;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/login_wallpaper.png'),
|
||||
fit: BoxFit.cover,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
colors: [
|
||||
colorScheme.primaryContainer.withAlpha(64),
|
||||
colorScheme.secondaryContainer.withAlpha(64),
|
||||
colorScheme.tertiaryContainer.withAlpha(64),
|
||||
colorScheme.primaryContainer.withAlpha(64),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 64),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Material(
|
||||
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.925),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
elevation: 10,
|
||||
shadowColor: Colors.black,
|
||||
elevation:
|
||||
Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4,
|
||||
shadowColor: Theme.of(context).appBarTheme.shadowColor,
|
||||
child: ConstrainedBox(
|
||||
constraints: isMobileMode
|
||||
? const BoxConstraints()
|
||||
: const BoxConstraints(maxWidth: 480, maxHeight: 640),
|
||||
: const BoxConstraints(maxWidth: 960, maxHeight: 640),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Image.asset(
|
||||
'assets/login_wallpaper.png',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerTheme.color,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: scaffold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const _PrivacyButtons(mainAxisAlignment: MainAxisAlignment.end),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PrivacyButtons extends StatelessWidget {
|
||||
final MainAxisAlignment mainAxisAlignment;
|
||||
const _PrivacyButtons({required this.mainAxisAlignment});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: mainAxisAlignment,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => PlatformInfos.showDialog(context),
|
||||
child: Text(L10n.of(context)!.about),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => launchUrlString(AppConfig.privacyUrl),
|
||||
child: Text(L10n.of(context)!.privacy),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user