mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-11-04 14:27:23 +01:00 
			
		
		
		
	Merge branch 'krille/new-onboarding-design' into 'main'
feat: New onboarding design See merge request famedly/fluffychat!829
This commit is contained in:
		
						commit
						ae2a44f936
					
				
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 36 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 211 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/login_wallpaper.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/login_wallpaper.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 212 KiB  | 
@ -9,6 +9,7 @@ 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';
 | 
			
		||||
@ -257,10 +258,21 @@ class AppRoutes {
 | 
			
		||||
              buildTransition: _fadeTransition,
 | 
			
		||||
            ),
 | 
			
		||||
            VWidget(
 | 
			
		||||
              path: 'signup',
 | 
			
		||||
              widget: const SignupPage(),
 | 
			
		||||
              buildTransition: _fadeTransition,
 | 
			
		||||
            ),
 | 
			
		||||
                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(),
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,43 @@ abstract class FluffyThemes {
 | 
			
		||||
  static const fallbackTextStyle =
 | 
			
		||||
      TextStyle(fontFamily: 'Roboto', fontFamilyFallback: ['NotoEmoji']);
 | 
			
		||||
 | 
			
		||||
  static InputDecoration loginTextFieldDecoration({
 | 
			
		||||
    String? errorText,
 | 
			
		||||
    String? labelText,
 | 
			
		||||
    String? hintText,
 | 
			
		||||
    Widget? suffixIcon,
 | 
			
		||||
    Widget? prefixIcon,
 | 
			
		||||
  }) =>
 | 
			
		||||
      InputDecoration(
 | 
			
		||||
        fillColor: Colors.white.withAlpha(200),
 | 
			
		||||
        labelText: labelText,
 | 
			
		||||
        hintText: hintText,
 | 
			
		||||
        suffixIcon: suffixIcon,
 | 
			
		||||
        prefixIcon: prefixIcon,
 | 
			
		||||
        errorText: errorText,
 | 
			
		||||
        errorStyle: TextStyle(
 | 
			
		||||
          color: Colors.red.shade200,
 | 
			
		||||
          shadows: const [
 | 
			
		||||
            Shadow(
 | 
			
		||||
              color: Colors.black,
 | 
			
		||||
              offset: Offset(0, 0),
 | 
			
		||||
              blurRadius: 5,
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        labelStyle: const TextStyle(
 | 
			
		||||
          color: Colors.white,
 | 
			
		||||
          shadows: [
 | 
			
		||||
            Shadow(
 | 
			
		||||
              color: Colors.black,
 | 
			
		||||
              offset: Offset(0, 0),
 | 
			
		||||
              blurRadius: 5,
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        contentPadding: const EdgeInsets.all(16),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
  static var fallbackTextTheme = const TextTheme(
 | 
			
		||||
    bodyText1: fallbackTextStyle,
 | 
			
		||||
    bodyText2: fallbackTextStyle,
 | 
			
		||||
@ -77,6 +114,7 @@ abstract class FluffyThemes {
 | 
			
		||||
          style: ElevatedButton.styleFrom(
 | 
			
		||||
            primary: AppConfig.chatColor,
 | 
			
		||||
            onPrimary: Colors.white,
 | 
			
		||||
            textStyle: const TextStyle(fontSize: 16),
 | 
			
		||||
            elevation: 6,
 | 
			
		||||
            shadowColor: const Color(0x44000000),
 | 
			
		||||
            minimumSize: const Size.fromHeight(48),
 | 
			
		||||
@ -186,6 +224,7 @@ abstract class FluffyThemes {
 | 
			
		||||
            primary: AppConfig.chatColor,
 | 
			
		||||
            onPrimary: Colors.white,
 | 
			
		||||
            minimumSize: const Size.fromHeight(48),
 | 
			
		||||
            textStyle: const TextStyle(fontSize: 16),
 | 
			
		||||
            shape: RoundedRectangleBorder(
 | 
			
		||||
              borderRadius: BorderRadius.circular(AppConfig.borderRadius),
 | 
			
		||||
            ),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										191
									
								
								lib/pages/connect/connect_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								lib/pages/connect/connect_page.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,191 @@
 | 
			
		||||
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/flutter_web_auth.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 =>
 | 
			
		||||
      (PlatformInfos.isMobile ||
 | 
			
		||||
          PlatformInfos.isWeb ||
 | 
			
		||||
          PlatformInfos.isMacOS) &&
 | 
			
		||||
      _supportsFlow('m.login.sso');
 | 
			
		||||
 | 
			
		||||
  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'
 | 
			
		||||
        : AppConfig.appOpenUrlScheme.toLowerCase() + '://login';
 | 
			
		||||
    final url =
 | 
			
		||||
        '${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}';
 | 
			
		||||
    final urlScheme = Uri.parse(redirectUrl).scheme;
 | 
			
		||||
    final result = await FlutterWebAuth.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'],
 | 
			
		||||
      );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										184
									
								
								lib/pages/connect/connect_page_view.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								lib/pages/connect/connect_page_view.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,184 @@
 | 
			
		||||
import 'dart:typed_data';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/config/themes.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(
 | 
			
		||||
        automaticallyImplyLeading: !controller.loading,
 | 
			
		||||
        backgroundColor: Colors.transparent,
 | 
			
		||||
        iconTheme: const IconThemeData(color: Colors.white),
 | 
			
		||||
        elevation: 0,
 | 
			
		||||
        centerTitle: true,
 | 
			
		||||
        title: Text(
 | 
			
		||||
          Matrix.of(context).getLoginClient().homeserver?.host ?? '',
 | 
			
		||||
          style: const TextStyle(color: Colors.white),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      body: ListView(
 | 
			
		||||
        children: [
 | 
			
		||||
          if (Matrix.of(context).loginRegistrationSupported ?? false) ...[
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.all(16.0),
 | 
			
		||||
              child: Center(
 | 
			
		||||
                child: Material(
 | 
			
		||||
                  borderRadius: BorderRadius.circular(64),
 | 
			
		||||
                  elevation: 10,
 | 
			
		||||
                  color: Colors.transparent,
 | 
			
		||||
                  child: CircleAvatar(
 | 
			
		||||
                    radius: 64,
 | 
			
		||||
                    backgroundColor: Colors.white.withAlpha(200),
 | 
			
		||||
                    child: Stack(
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Center(
 | 
			
		||||
                          child: avatar == null
 | 
			
		||||
                              ? const Icon(
 | 
			
		||||
                                  Icons.person_outlined,
 | 
			
		||||
                                  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);
 | 
			
		||||
                                  },
 | 
			
		||||
                                ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        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(16.0),
 | 
			
		||||
              child: TextField(
 | 
			
		||||
                controller: controller.usernameController,
 | 
			
		||||
                onSubmitted: (_) => controller.signUp(),
 | 
			
		||||
                decoration: FluffyThemes.loginTextFieldDecoration(
 | 
			
		||||
                  prefixIcon: const Icon(Icons.account_box_outlined),
 | 
			
		||||
                  hintText: L10n.of(context)!.chooseAUsername,
 | 
			
		||||
                  errorText: controller.signupError,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.all(16.0),
 | 
			
		||||
              child: Hero(
 | 
			
		||||
                tag: 'loginButton',
 | 
			
		||||
                child: ElevatedButton(
 | 
			
		||||
                  onPressed: controller.loading ? null : controller.signUp,
 | 
			
		||||
                  style: ElevatedButton.styleFrom(
 | 
			
		||||
                    primary: Colors.white.withAlpha(200),
 | 
			
		||||
                    onPrimary: Colors.black,
 | 
			
		||||
                    shadowColor: Colors.white,
 | 
			
		||||
                  ),
 | 
			
		||||
                  child: controller.loading
 | 
			
		||||
                      ? const LinearProgressIndicator()
 | 
			
		||||
                      : Text(L10n.of(context)!.signUp),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            Row(
 | 
			
		||||
              children: [
 | 
			
		||||
                const Expanded(child: Divider(color: Colors.white)),
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.all(16.0),
 | 
			
		||||
                  child: Text(
 | 
			
		||||
                    L10n.of(context)!.or,
 | 
			
		||||
                    style: const TextStyle(color: Colors.white),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const Expanded(child: Divider(color: Colors.white)),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
          if (controller.supportsSso)
 | 
			
		||||
            identityProviders == null
 | 
			
		||||
                ? const SizedBox(
 | 
			
		||||
                    height: 74,
 | 
			
		||||
                    child: Center(
 | 
			
		||||
                        child: CircularProgressIndicator.adaptive(
 | 
			
		||||
                      backgroundColor: Colors.white,
 | 
			
		||||
                    )),
 | 
			
		||||
                  )
 | 
			
		||||
                : Center(
 | 
			
		||||
                    child: identityProviders.length == 1
 | 
			
		||||
                        ? Padding(
 | 
			
		||||
                            padding: const EdgeInsets.all(16.0),
 | 
			
		||||
                            child: ElevatedButton(
 | 
			
		||||
                              onPressed: () => controller
 | 
			
		||||
                                  .ssoLoginAction(identityProviders.single.id!),
 | 
			
		||||
                              style: ElevatedButton.styleFrom(
 | 
			
		||||
                                primary: Colors.white.withAlpha(200),
 | 
			
		||||
                                onPrimary: Colors.black,
 | 
			
		||||
                                shadowColor: Colors.white,
 | 
			
		||||
                              ),
 | 
			
		||||
                              child: 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(16.0),
 | 
			
		||||
              child: Hero(
 | 
			
		||||
                tag: 'signinButton',
 | 
			
		||||
                child: ElevatedButton(
 | 
			
		||||
                  onPressed: controller.loading ? () {} : controller.login,
 | 
			
		||||
                  style: ElevatedButton.styleFrom(
 | 
			
		||||
                    primary: Colors.white.withAlpha(200),
 | 
			
		||||
                    onPrimary: Colors.black,
 | 
			
		||||
                    shadowColor: Colors.white,
 | 
			
		||||
                  ),
 | 
			
		||||
                  child: Text(L10n.of(context)!.login),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								lib/pages/connect/sso_button.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								lib/pages/connect/sso_button.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.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: 12.0, vertical: 8.0),
 | 
			
		||||
        child: Column(
 | 
			
		||||
          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            Material(
 | 
			
		||||
              color: Colors.white,
 | 
			
		||||
              borderRadius: BorderRadius.circular(7),
 | 
			
		||||
              clipBehavior: Clip.hardEdge,
 | 
			
		||||
              child: Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(2.0),
 | 
			
		||||
                child: identityProvider.icon == null
 | 
			
		||||
                    ? const Icon(Icons.web_outlined)
 | 
			
		||||
                    : CachedNetworkImage(
 | 
			
		||||
                        imageUrl: 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,
 | 
			
		||||
                color: Colors.white,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,19 +1,12 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import 'package:flutter_web_auth/flutter_web_auth.dart';
 | 
			
		||||
import 'package:future_loading_dialog/future_loading_dialog.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/homeserver_picker/homeserver_picker_view.dart';
 | 
			
		||||
import 'package:fluffychat/utils/famedlysdk_store.dart';
 | 
			
		||||
import 'package:fluffychat/utils/platform_infos.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
import '../../utils/localized_exception_extension.dart';
 | 
			
		||||
 | 
			
		||||
@ -26,182 +19,58 @@ class HomeserverPicker extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
class HomeserverPickerController extends State<HomeserverPicker> {
 | 
			
		||||
  bool isLoading = false;
 | 
			
		||||
  String domain = AppConfig.defaultHomeserver;
 | 
			
		||||
  final TextEditingController homeserverController =
 | 
			
		||||
      TextEditingController(text: AppConfig.defaultHomeserver);
 | 
			
		||||
  StreamSubscription? _intentDataStreamSubscription;
 | 
			
		||||
  String? error;
 | 
			
		||||
  Timer? _coolDown;
 | 
			
		||||
 | 
			
		||||
  void setDomain(String domain) {
 | 
			
		||||
    this.domain = domain;
 | 
			
		||||
    _coolDown?.cancel();
 | 
			
		||||
    if (domain.isNotEmpty) {
 | 
			
		||||
      _coolDown =
 | 
			
		||||
          Timer(const Duration(milliseconds: 500), checkHomeserverAction);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _loginWithToken(String token) {
 | 
			
		||||
    if (token.isEmpty) return;
 | 
			
		||||
 | 
			
		||||
    showFutureLoadingDialog(
 | 
			
		||||
      context: context,
 | 
			
		||||
      future: () async {
 | 
			
		||||
        if (Matrix.of(context).getLoginClient().homeserver == null) {
 | 
			
		||||
          await Matrix.of(context).getLoginClient().checkHomeserver(
 | 
			
		||||
                await Store()
 | 
			
		||||
                    .getItem(HomeserverPickerController.ssoHomeserverKey),
 | 
			
		||||
              );
 | 
			
		||||
        }
 | 
			
		||||
        await Matrix.of(context).getLoginClient().login(
 | 
			
		||||
              LoginType.mLoginToken,
 | 
			
		||||
              token: token,
 | 
			
		||||
              initialDeviceDisplayName: PlatformInfos.clientName,
 | 
			
		||||
            );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    checkHomeserverAction();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    super.dispose();
 | 
			
		||||
    _intentDataStreamSubscription?.cancel();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String? _lastCheckedHomeserver;
 | 
			
		||||
 | 
			
		||||
  /// 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 {
 | 
			
		||||
    _coolDown?.cancel();
 | 
			
		||||
    if (_lastCheckedHomeserver == domain) return;
 | 
			
		||||
    if (domain.isEmpty) throw L10n.of(context)!.changeTheHomeserver;
 | 
			
		||||
    var homeserver = domain;
 | 
			
		||||
 | 
			
		||||
    if (!homeserver.startsWith('https://')) {
 | 
			
		||||
      homeserver = 'https://$homeserver';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setState(() {
 | 
			
		||||
      error = _rawLoginTypes = registrationSupported = null;
 | 
			
		||||
      error = null;
 | 
			
		||||
      isLoading = true;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await Matrix.of(context).getLoginClient().checkHomeserver(homeserver);
 | 
			
		||||
      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');
 | 
			
		||||
 | 
			
		||||
      _rawLoginTypes = await Matrix.of(context).getLoginClient().request(
 | 
			
		||||
            RequestType.GET,
 | 
			
		||||
            '/client/r0/login',
 | 
			
		||||
          );
 | 
			
		||||
      try {
 | 
			
		||||
        await Matrix.of(context).getLoginClient().register();
 | 
			
		||||
        registrationSupported = true;
 | 
			
		||||
        matrix.loginRegistrationSupported = true;
 | 
			
		||||
      } on MatrixException catch (e) {
 | 
			
		||||
        registrationSupported = e.requireAdditionalAuthentication;
 | 
			
		||||
        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');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      setState(() => error = (e).toLocalizedString(context));
 | 
			
		||||
    } finally {
 | 
			
		||||
      _lastCheckedHomeserver = domain;
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        setState(() => isLoading = false);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic>? _rawLoginTypes;
 | 
			
		||||
  bool? registrationSupported;
 | 
			
		||||
 | 
			
		||||
  List<IdentityProvider> get identityProviders {
 | 
			
		||||
    if (!ssoLoginSupported) return [];
 | 
			
		||||
    final rawProviders = _rawLoginTypes!.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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get passwordLoginSupported =>
 | 
			
		||||
      Matrix.of(context)
 | 
			
		||||
          .client
 | 
			
		||||
          .supportedLoginTypes
 | 
			
		||||
          .contains(AuthenticationTypes.password) &&
 | 
			
		||||
      _rawLoginTypes!
 | 
			
		||||
          .tryGetList('flows')!
 | 
			
		||||
          .any((flow) => flow['type'] == AuthenticationTypes.password);
 | 
			
		||||
 | 
			
		||||
  bool get ssoLoginSupported =>
 | 
			
		||||
      Matrix.of(context)
 | 
			
		||||
          .client
 | 
			
		||||
          .supportedLoginTypes
 | 
			
		||||
          .contains(AuthenticationTypes.sso) &&
 | 
			
		||||
      _rawLoginTypes!
 | 
			
		||||
          .tryGetList('flows')!
 | 
			
		||||
          .any((flow) => flow['type'] == AuthenticationTypes.sso);
 | 
			
		||||
 | 
			
		||||
  static const String ssoHomeserverKey = 'sso-homeserver';
 | 
			
		||||
 | 
			
		||||
  void ssoLoginAction(String id) async {
 | 
			
		||||
    if (kIsWeb) {
 | 
			
		||||
      // We store the homserver in the local storage instead of a redirect
 | 
			
		||||
      // parameter because of possible CSRF attacks.
 | 
			
		||||
      Store().setItem(ssoHomeserverKey,
 | 
			
		||||
          Matrix.of(context).getLoginClient().homeserver.toString());
 | 
			
		||||
    }
 | 
			
		||||
    final redirectUrl = kIsWeb
 | 
			
		||||
        ? html.window.origin! + '/web/auth.html'
 | 
			
		||||
        : AppConfig.appOpenUrlScheme.toLowerCase() + '://login';
 | 
			
		||||
    final url =
 | 
			
		||||
        '${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}';
 | 
			
		||||
    final urlScheme = Uri.parse(redirectUrl).scheme;
 | 
			
		||||
    final result = await FlutterWebAuth.authenticate(
 | 
			
		||||
      url: url,
 | 
			
		||||
      callbackUrlScheme: urlScheme,
 | 
			
		||||
    );
 | 
			
		||||
    final token = Uri.parse(result).queryParameters['loginToken'];
 | 
			
		||||
    if (token != null) _loginWithToken(token);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void signUpAction() => VRouter.of(context).to(
 | 
			
		||||
        'signup',
 | 
			
		||||
        queryParameters: {'domain': domain},
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    Matrix.of(context).navigatorContext = context;
 | 
			
		||||
    return HomeserverPickerView(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,17 +1,13 @@
 | 
			
		||||
import 'package:flutter/cupertino.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher.dart';
 | 
			
		||||
import 'package:vrouter/vrouter.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/config/app_config.dart';
 | 
			
		||||
import 'package:fluffychat/config/themes.dart';
 | 
			
		||||
import 'package:fluffychat/utils/platform_infos.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/default_app_bar_search_field.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/layouts/one_page_card.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
 | 
			
		||||
import 'homeserver_picker.dart';
 | 
			
		||||
 | 
			
		||||
class HomeserverPickerView extends StatelessWidget {
 | 
			
		||||
@ -21,237 +17,86 @@ class HomeserverPickerView extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return OnePageCard(
 | 
			
		||||
      child: Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          titleSpacing: 8,
 | 
			
		||||
          title: DefaultAppBarSearchField(
 | 
			
		||||
            prefixText: 'https://',
 | 
			
		||||
            hintText: L10n.of(context)!.enterYourHomeserver,
 | 
			
		||||
            searchController: controller.homeserverController,
 | 
			
		||||
            suffix: const Icon(Icons.edit_outlined),
 | 
			
		||||
            padding: EdgeInsets.zero,
 | 
			
		||||
            onChanged: controller.setDomain,
 | 
			
		||||
            readOnly: !AppConfig.allowOtherHomeservers,
 | 
			
		||||
            onSubmit: (_) => controller.checkHomeserverAction(),
 | 
			
		||||
            unfocusOnClear: false,
 | 
			
		||||
            autocorrect: false,
 | 
			
		||||
            labelText: L10n.of(context)!.homeserver,
 | 
			
		||||
          ),
 | 
			
		||||
          elevation: 0,
 | 
			
		||||
        ),
 | 
			
		||||
        body: ListView(children: [
 | 
			
		||||
          Image.asset(
 | 
			
		||||
            Theme.of(context).brightness == Brightness.dark
 | 
			
		||||
                ? 'assets/banner_dark.png'
 | 
			
		||||
                : 'assets/banner.png',
 | 
			
		||||
          ),
 | 
			
		||||
          if (controller.isLoading)
 | 
			
		||||
            const Center(
 | 
			
		||||
                child: CircularProgressIndicator.adaptive(strokeWidth: 2))
 | 
			
		||||
          else if (controller.error != null)
 | 
			
		||||
            Center(
 | 
			
		||||
              child: Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(12.0),
 | 
			
		||||
                child: Text(
 | 
			
		||||
                  controller.error!,
 | 
			
		||||
                  textAlign: TextAlign.center,
 | 
			
		||||
                  style: TextStyle(
 | 
			
		||||
                    fontSize: 18,
 | 
			
		||||
                    color: Colors.red[900],
 | 
			
		||||
    return LoginScaffold(
 | 
			
		||||
      appBar: VRouter.of(context).path == '/home'
 | 
			
		||||
          ? null
 | 
			
		||||
          : AppBar(title: Text(L10n.of(context)!.addAccount)),
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: ListView(
 | 
			
		||||
              children: [
 | 
			
		||||
                Center(
 | 
			
		||||
                  child: ConstrainedBox(
 | 
			
		||||
                    constraints: const BoxConstraints(maxHeight: 256),
 | 
			
		||||
                    child: Image.asset(
 | 
			
		||||
                      'assets/info-logo.png',
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            )
 | 
			
		||||
          else
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.symmetric(
 | 
			
		||||
                horizontal: 12.0,
 | 
			
		||||
                vertical: 4.0,
 | 
			
		||||
              ),
 | 
			
		||||
              child: Column(
 | 
			
		||||
                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                children: [
 | 
			
		||||
                  if (controller.ssoLoginSupported)
 | 
			
		||||
                    Row(children: [
 | 
			
		||||
                      const Expanded(child: Divider()),
 | 
			
		||||
                      Padding(
 | 
			
		||||
                        padding: const EdgeInsets.all(12.0),
 | 
			
		||||
                        child: Text(L10n.of(context)!.loginWithOneClick),
 | 
			
		||||
                      ),
 | 
			
		||||
                      const Expanded(child: Divider()),
 | 
			
		||||
                    ]),
 | 
			
		||||
                  Wrap(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      if (controller.ssoLoginSupported) ...{
 | 
			
		||||
                        for (final identityProvider
 | 
			
		||||
                            in controller.identityProviders)
 | 
			
		||||
                          _SsoButton(
 | 
			
		||||
                            onPressed: () =>
 | 
			
		||||
                                controller.ssoLoginAction(identityProvider.id!),
 | 
			
		||||
                            identityProvider: identityProvider,
 | 
			
		||||
                          ),
 | 
			
		||||
                      },
 | 
			
		||||
                    ].toList(),
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.all(16.0),
 | 
			
		||||
                  child: TextField(
 | 
			
		||||
                    controller: controller.homeserverController,
 | 
			
		||||
                    decoration: FluffyThemes.loginTextFieldDecoration(
 | 
			
		||||
                      labelText: L10n.of(context)!.homeserver,
 | 
			
		||||
                      hintText: L10n.of(context)!.enterYourHomeserver,
 | 
			
		||||
                      suffixIcon: const Icon(Icons.search),
 | 
			
		||||
                      errorText: controller.error,
 | 
			
		||||
                    ),
 | 
			
		||||
                    readOnly: !AppConfig.allowOtherHomeservers,
 | 
			
		||||
                    onSubmitted: (_) => controller.checkHomeserverAction(),
 | 
			
		||||
                    autocorrect: false,
 | 
			
		||||
                  ),
 | 
			
		||||
                  if (controller.ssoLoginSupported &&
 | 
			
		||||
                      (controller.registrationSupported! ||
 | 
			
		||||
                          controller.passwordLoginSupported))
 | 
			
		||||
                    Row(children: [
 | 
			
		||||
                      const Expanded(child: Divider()),
 | 
			
		||||
                      Padding(
 | 
			
		||||
                        padding: const EdgeInsets.all(12.0),
 | 
			
		||||
                        child: Text(L10n.of(context)!.or),
 | 
			
		||||
                      ),
 | 
			
		||||
                      const Expanded(child: Divider()),
 | 
			
		||||
                    ]),
 | 
			
		||||
                  if (controller.passwordLoginSupported) ...[
 | 
			
		||||
                    Center(
 | 
			
		||||
                      child: _LoginButton(
 | 
			
		||||
                        onPressed: () => VRouter.of(context).to('login'),
 | 
			
		||||
                        icon: Icon(
 | 
			
		||||
                          CupertinoIcons.lock_open_fill,
 | 
			
		||||
                          color: Theme.of(context).textTheme.bodyText1!.color,
 | 
			
		||||
                ),
 | 
			
		||||
                Wrap(
 | 
			
		||||
                  alignment: WrapAlignment.center,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    TextButton(
 | 
			
		||||
                      onPressed: () => launch(AppConfig.privacyUrl),
 | 
			
		||||
                      child: Text(
 | 
			
		||||
                        L10n.of(context)!.privacy,
 | 
			
		||||
                        style: const TextStyle(
 | 
			
		||||
                          decoration: TextDecoration.underline,
 | 
			
		||||
                          color: Colors.white,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    TextButton(
 | 
			
		||||
                      onPressed: () => PlatformInfos.showDialog(context),
 | 
			
		||||
                      child: Text(
 | 
			
		||||
                        L10n.of(context)!.about,
 | 
			
		||||
                        style: const TextStyle(
 | 
			
		||||
                          decoration: TextDecoration.underline,
 | 
			
		||||
                          color: Colors.white,
 | 
			
		||||
                        ),
 | 
			
		||||
                        labelText: L10n.of(context)!.login,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const SizedBox(height: 12),
 | 
			
		||||
                  ],
 | 
			
		||||
                  if (controller.registrationSupported!)
 | 
			
		||||
                    Center(
 | 
			
		||||
                      child: _LoginButton(
 | 
			
		||||
                        onPressed: controller.signUpAction,
 | 
			
		||||
                        icon: Icon(
 | 
			
		||||
                          CupertinoIcons.person_add,
 | 
			
		||||
                          color: Theme.of(context).textTheme.bodyText1!.color,
 | 
			
		||||
                        ),
 | 
			
		||||
                        labelText: L10n.of(context)!.register,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
        ]),
 | 
			
		||||
        bottomNavigationBar: Material(
 | 
			
		||||
          elevation: 6,
 | 
			
		||||
          color: Theme.of(context).scaffoldBackgroundColor,
 | 
			
		||||
          child: Wrap(
 | 
			
		||||
            alignment: WrapAlignment.center,
 | 
			
		||||
            children: [
 | 
			
		||||
              TextButton(
 | 
			
		||||
                onPressed: () => launch(AppConfig.privacyUrl),
 | 
			
		||||
                child: Text(
 | 
			
		||||
                  L10n.of(context)!.privacy,
 | 
			
		||||
                  style: const TextStyle(
 | 
			
		||||
                    decoration: TextDecoration.underline,
 | 
			
		||||
                    color: Colors.blueGrey,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              TextButton(
 | 
			
		||||
                onPressed: () => PlatformInfos.showDialog(context),
 | 
			
		||||
                child: Text(
 | 
			
		||||
                  L10n.of(context)!.about,
 | 
			
		||||
                  style: const TextStyle(
 | 
			
		||||
                    decoration: TextDecoration.underline,
 | 
			
		||||
                    color: Colors.blueGrey,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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: 12.0, vertical: 8.0),
 | 
			
		||||
        child: Column(
 | 
			
		||||
          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            Material(
 | 
			
		||||
              color: Colors.white,
 | 
			
		||||
              borderRadius: BorderRadius.circular(7),
 | 
			
		||||
              clipBehavior: Clip.hardEdge,
 | 
			
		||||
              child: Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(2.0),
 | 
			
		||||
                child: identityProvider.icon == null
 | 
			
		||||
                    ? const Icon(Icons.web_outlined)
 | 
			
		||||
                    : CachedNetworkImage(
 | 
			
		||||
                        imageUrl: Uri.parse(identityProvider.icon!)
 | 
			
		||||
                            .getDownloadLink(
 | 
			
		||||
                                Matrix.of(context).getLoginClient())
 | 
			
		||||
                            .toString(),
 | 
			
		||||
                        width: 32,
 | 
			
		||||
                        height: 32,
 | 
			
		||||
                      ),
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.all(16),
 | 
			
		||||
            child: Hero(
 | 
			
		||||
              tag: 'loginButton',
 | 
			
		||||
              child: ElevatedButton(
 | 
			
		||||
                onPressed: controller.isLoading
 | 
			
		||||
                    ? () {}
 | 
			
		||||
                    : controller.checkHomeserverAction,
 | 
			
		||||
                style: ElevatedButton.styleFrom(
 | 
			
		||||
                  primary: Colors.white.withAlpha(200),
 | 
			
		||||
                  onPrimary: Colors.black,
 | 
			
		||||
                  shadowColor: Colors.white,
 | 
			
		||||
                ),
 | 
			
		||||
                child: controller.isLoading
 | 
			
		||||
                    ? const LinearProgressIndicator()
 | 
			
		||||
                    : Text(L10n.of(context)!.connect),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            const SizedBox(height: 8),
 | 
			
		||||
            Text(
 | 
			
		||||
              identityProvider.name ??
 | 
			
		||||
                  identityProvider.brand ??
 | 
			
		||||
                  L10n.of(context)!.singlesignon,
 | 
			
		||||
              style: TextStyle(
 | 
			
		||||
                fontSize: 12,
 | 
			
		||||
                fontWeight: FontWeight.bold,
 | 
			
		||||
                color: Theme.of(context).textTheme.subtitle2!.color,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _LoginButton extends StatelessWidget {
 | 
			
		||||
  final String? labelText;
 | 
			
		||||
  final Widget? icon;
 | 
			
		||||
  final void Function()? onPressed;
 | 
			
		||||
  const _LoginButton({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    this.labelText,
 | 
			
		||||
    this.icon,
 | 
			
		||||
    this.onPressed,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return OutlinedButton.icon(
 | 
			
		||||
      style: OutlinedButton.styleFrom(
 | 
			
		||||
        minimumSize: const Size(256, 56),
 | 
			
		||||
        textStyle: const TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
        backgroundColor: Theme.of(context).backgroundColor,
 | 
			
		||||
        shape: RoundedRectangleBorder(
 | 
			
		||||
          borderRadius: BorderRadius.circular(AppConfig.borderRadius),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      onPressed: onPressed,
 | 
			
		||||
      icon: icon!,
 | 
			
		||||
      label: Text(
 | 
			
		||||
        labelText!,
 | 
			
		||||
        style: TextStyle(
 | 
			
		||||
          color: Theme.of(context).textTheme.bodyText1!.color,
 | 
			
		||||
        ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/widgets/layouts/one_page_card.dart';
 | 
			
		||||
import 'package:fluffychat/config/themes.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
import 'login.dart';
 | 
			
		||||
 | 
			
		||||
@ -13,102 +14,119 @@ class LoginView extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return OnePageCard(
 | 
			
		||||
      child: Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: controller.loading ? Container() : const BackButton(),
 | 
			
		||||
          elevation: 0,
 | 
			
		||||
          title: Text(
 | 
			
		||||
            L10n.of(context)!.logInTo(Matrix.of(context)
 | 
			
		||||
                .getLoginClient()
 | 
			
		||||
                .homeserver
 | 
			
		||||
                .toString()
 | 
			
		||||
                .replaceFirst('https://', '')),
 | 
			
		||||
          ),
 | 
			
		||||
    return LoginScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        automaticallyImplyLeading: !controller.loading,
 | 
			
		||||
        backgroundColor: Colors.transparent,
 | 
			
		||||
        iconTheme: const IconThemeData(color: Colors.white),
 | 
			
		||||
        elevation: 0,
 | 
			
		||||
        centerTitle: true,
 | 
			
		||||
        title: Text(
 | 
			
		||||
          L10n.of(context)!.logInTo(Matrix.of(context)
 | 
			
		||||
              .getLoginClient()
 | 
			
		||||
              .homeserver
 | 
			
		||||
              .toString()
 | 
			
		||||
              .replaceFirst('https://', '')),
 | 
			
		||||
          style: const TextStyle(color: Colors.white),
 | 
			
		||||
        ),
 | 
			
		||||
        body: Builder(builder: (context) {
 | 
			
		||||
          return AutofillGroup(
 | 
			
		||||
            child: ListView(
 | 
			
		||||
              children: <Widget>[
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.all(12.0),
 | 
			
		||||
                  child: TextField(
 | 
			
		||||
                    readOnly: controller.loading,
 | 
			
		||||
                    autocorrect: false,
 | 
			
		||||
                    autofocus: true,
 | 
			
		||||
                    onChanged: controller.checkWellKnownWithCoolDown,
 | 
			
		||||
                    controller: controller.usernameController,
 | 
			
		||||
                    textInputAction: TextInputAction.next,
 | 
			
		||||
                    keyboardType: TextInputType.emailAddress,
 | 
			
		||||
                    autofillHints:
 | 
			
		||||
                        controller.loading ? null : [AutofillHints.username],
 | 
			
		||||
                    decoration: InputDecoration(
 | 
			
		||||
                        prefixIcon: const Icon(Icons.account_box_outlined),
 | 
			
		||||
                        hintText: L10n.of(context)!.emailOrUsername,
 | 
			
		||||
                        errorText: controller.usernameError,
 | 
			
		||||
                        labelText: L10n.of(context)!.emailOrUsername),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Builder(builder: (context) {
 | 
			
		||||
        return AutofillGroup(
 | 
			
		||||
          child: ListView(
 | 
			
		||||
            children: <Widget>[
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(16.0),
 | 
			
		||||
                child: TextField(
 | 
			
		||||
                  readOnly: controller.loading,
 | 
			
		||||
                  autocorrect: false,
 | 
			
		||||
                  autofocus: true,
 | 
			
		||||
                  onChanged: controller.checkWellKnownWithCoolDown,
 | 
			
		||||
                  controller: controller.usernameController,
 | 
			
		||||
                  textInputAction: TextInputAction.next,
 | 
			
		||||
                  keyboardType: TextInputType.emailAddress,
 | 
			
		||||
                  autofillHints:
 | 
			
		||||
                      controller.loading ? null : [AutofillHints.username],
 | 
			
		||||
                  decoration: FluffyThemes.loginTextFieldDecoration(
 | 
			
		||||
                    prefixIcon: const Icon(Icons.account_box_outlined),
 | 
			
		||||
                    errorText: controller.usernameError,
 | 
			
		||||
                    hintText: L10n.of(context)!.emailOrUsername,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.all(12.0),
 | 
			
		||||
                  child: TextField(
 | 
			
		||||
                    readOnly: controller.loading,
 | 
			
		||||
                    autocorrect: false,
 | 
			
		||||
                    autofillHints:
 | 
			
		||||
                        controller.loading ? null : [AutofillHints.password],
 | 
			
		||||
                    controller: controller.passwordController,
 | 
			
		||||
                    textInputAction: TextInputAction.next,
 | 
			
		||||
                    obscureText: !controller.showPassword,
 | 
			
		||||
                    onSubmitted: controller.login,
 | 
			
		||||
                    decoration: InputDecoration(
 | 
			
		||||
                      prefixIcon: const Icon(Icons.lock_outlined),
 | 
			
		||||
                      hintText: '****',
 | 
			
		||||
                      errorText: controller.passwordError,
 | 
			
		||||
                      suffixIcon: IconButton(
 | 
			
		||||
                        tooltip: L10n.of(context)!.showPassword,
 | 
			
		||||
                        icon: Icon(controller.showPassword
 | 
			
		||||
                            ? Icons.visibility_off_outlined
 | 
			
		||||
                            : Icons.visibility_outlined),
 | 
			
		||||
                        onPressed: controller.toggleShowPassword,
 | 
			
		||||
                      ),
 | 
			
		||||
                      labelText: L10n.of(context)!.password,
 | 
			
		||||
              ),
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(16.0),
 | 
			
		||||
                child: TextField(
 | 
			
		||||
                  readOnly: controller.loading,
 | 
			
		||||
                  autocorrect: false,
 | 
			
		||||
                  autofillHints:
 | 
			
		||||
                      controller.loading ? null : [AutofillHints.password],
 | 
			
		||||
                  controller: controller.passwordController,
 | 
			
		||||
                  textInputAction: TextInputAction.next,
 | 
			
		||||
                  obscureText: !controller.showPassword,
 | 
			
		||||
                  onSubmitted: controller.login,
 | 
			
		||||
                  decoration: FluffyThemes.loginTextFieldDecoration(
 | 
			
		||||
                    prefixIcon: const Icon(Icons.lock_outlined),
 | 
			
		||||
                    errorText: controller.passwordError,
 | 
			
		||||
                    suffixIcon: IconButton(
 | 
			
		||||
                      tooltip: L10n.of(context)!.showPassword,
 | 
			
		||||
                      icon: Icon(controller.showPassword
 | 
			
		||||
                          ? Icons.visibility_off_outlined
 | 
			
		||||
                          : Icons.visibility_outlined),
 | 
			
		||||
                      onPressed: controller.toggleShowPassword,
 | 
			
		||||
                    ),
 | 
			
		||||
                    hintText: L10n.of(context)!.password,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const SizedBox(height: 12),
 | 
			
		||||
                Hero(
 | 
			
		||||
                  tag: 'loginButton',
 | 
			
		||||
                  child: Padding(
 | 
			
		||||
                    padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
                    child: ElevatedButton(
 | 
			
		||||
                      onPressed: controller.loading
 | 
			
		||||
                          ? null
 | 
			
		||||
                          : () => controller.login(context),
 | 
			
		||||
                      child: controller.loading
 | 
			
		||||
                          ? const LinearProgressIndicator()
 | 
			
		||||
                          : Text(L10n.of(context)!.login),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const Divider(height: 32),
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
              ),
 | 
			
		||||
              Hero(
 | 
			
		||||
                tag: 'signinButton',
 | 
			
		||||
                child: Padding(
 | 
			
		||||
                  padding: const EdgeInsets.all(16),
 | 
			
		||||
                  child: ElevatedButton(
 | 
			
		||||
                    onPressed: controller.loading
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : controller.passwordForgotten,
 | 
			
		||||
                        : () => controller.login(context),
 | 
			
		||||
                    style: ElevatedButton.styleFrom(
 | 
			
		||||
                      primary: Theme.of(context).secondaryHeaderColor,
 | 
			
		||||
                      onPrimary: Colors.red,
 | 
			
		||||
                      primary: Colors.white.withAlpha(200),
 | 
			
		||||
                      onPrimary: Colors.black,
 | 
			
		||||
                      shadowColor: Colors.white,
 | 
			
		||||
                    ),
 | 
			
		||||
                    child: Text(L10n.of(context)!.passwordForgotten),
 | 
			
		||||
                    child: controller.loading
 | 
			
		||||
                        ? const LinearProgressIndicator()
 | 
			
		||||
                        : Text(L10n.of(context)!.login),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }),
 | 
			
		||||
      ),
 | 
			
		||||
              ),
 | 
			
		||||
              Row(
 | 
			
		||||
                children: [
 | 
			
		||||
                  const Expanded(child: Divider(color: Colors.white)),
 | 
			
		||||
                  Padding(
 | 
			
		||||
                    padding: const EdgeInsets.all(16.0),
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      L10n.of(context)!.or,
 | 
			
		||||
                      style: const TextStyle(color: Colors.white),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  const Expanded(child: Divider(color: Colors.white)),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(16),
 | 
			
		||||
                child: ElevatedButton(
 | 
			
		||||
                  onPressed:
 | 
			
		||||
                      controller.loading ? () {} : controller.passwordForgotten,
 | 
			
		||||
                  style: ElevatedButton.styleFrom(
 | 
			
		||||
                    primary: Colors.white.withAlpha(156),
 | 
			
		||||
                    onPrimary: Colors.red,
 | 
			
		||||
                    shadowColor: Colors.white,
 | 
			
		||||
                  ),
 | 
			
		||||
                  child: Text(L10n.of(context)!.passwordForgotten),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,13 +16,11 @@ class SignupPage extends StatefulWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SignupPageController extends State<SignupPage> {
 | 
			
		||||
  final TextEditingController usernameController = TextEditingController();
 | 
			
		||||
  final TextEditingController passwordController = TextEditingController();
 | 
			
		||||
  final TextEditingController passwordController2 = TextEditingController();
 | 
			
		||||
  final TextEditingController emailController = TextEditingController();
 | 
			
		||||
  String? error;
 | 
			
		||||
  bool loading = false;
 | 
			
		||||
  bool showPassword = false;
 | 
			
		||||
  bool showPassword = true;
 | 
			
		||||
 | 
			
		||||
  void toggleShowPassword() => setState(() => showPassword = !showPassword);
 | 
			
		||||
 | 
			
		||||
@ -30,15 +28,6 @@ class SignupPageController extends State<SignupPage> {
 | 
			
		||||
 | 
			
		||||
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();
 | 
			
		||||
 | 
			
		||||
  String? usernameTextFieldValidator(String? value) {
 | 
			
		||||
    usernameController.text =
 | 
			
		||||
        usernameController.text.trim().toLowerCase().replaceAll(' ', '_');
 | 
			
		||||
    if (value!.isEmpty) {
 | 
			
		||||
      return L10n.of(context)!.pleaseChooseAUsername;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String? password1TextFieldValidator(String? value) {
 | 
			
		||||
    const minLength = 8;
 | 
			
		||||
    if (value!.isEmpty) {
 | 
			
		||||
@ -50,18 +39,11 @@ class SignupPageController extends State<SignupPage> {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String? password2TextFieldValidator(String? value) {
 | 
			
		||||
    if (value!.isEmpty) {
 | 
			
		||||
      return L10n.of(context)!.chooseAStrongPassword;
 | 
			
		||||
    }
 | 
			
		||||
    if (value != passwordController.text) {
 | 
			
		||||
      return L10n.of(context)!.passwordsDoNotMatch;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String? emailTextFieldValidator(String? value) {
 | 
			
		||||
    if (value!.isNotEmpty && !value.contains('@')) {
 | 
			
		||||
    if (value!.isEmpty) {
 | 
			
		||||
      return L10n.of(context)!.addEmail;
 | 
			
		||||
    }
 | 
			
		||||
    if (value.isNotEmpty && !value.contains('@')) {
 | 
			
		||||
      return L10n.of(context)!.pleaseEnterValidEmail;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
@ -92,7 +74,7 @@ class SignupPageController extends State<SignupPage> {
 | 
			
		||||
      }
 | 
			
		||||
      await client.uiaRequestBackground(
 | 
			
		||||
        (auth) => client.register(
 | 
			
		||||
          username: usernameController.text,
 | 
			
		||||
          username: Matrix.of(context).loginUsername!,
 | 
			
		||||
          password: passwordController.text,
 | 
			
		||||
          initialDeviceDisplayName: PlatformInfos.clientName,
 | 
			
		||||
          auth: auth,
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/widgets/layouts/one_page_card.dart';
 | 
			
		||||
import 'package:fluffychat/config/themes.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
 | 
			
		||||
import 'signup.dart';
 | 
			
		||||
 | 
			
		||||
class SignupPageView extends StatelessWidget {
 | 
			
		||||
@ -11,121 +12,78 @@ class SignupPageView extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return OnePageCard(
 | 
			
		||||
      child: Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          title: Text(L10n.of(context)!.signUp),
 | 
			
		||||
    return LoginScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        automaticallyImplyLeading: !controller.loading,
 | 
			
		||||
        backgroundColor: Colors.transparent,
 | 
			
		||||
        iconTheme: const IconThemeData(color: Colors.white),
 | 
			
		||||
        elevation: 0,
 | 
			
		||||
        title: Text(
 | 
			
		||||
          L10n.of(context)!.signUp,
 | 
			
		||||
          style: const TextStyle(color: Colors.white),
 | 
			
		||||
        ),
 | 
			
		||||
        body: Form(
 | 
			
		||||
          key: controller.formKey,
 | 
			
		||||
          child: ListView(
 | 
			
		||||
            children: [
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(12.0),
 | 
			
		||||
                child: TextFormField(
 | 
			
		||||
                  readOnly: controller.loading,
 | 
			
		||||
                  autocorrect: false,
 | 
			
		||||
                  controller: controller.usernameController,
 | 
			
		||||
                  autofillHints:
 | 
			
		||||
                      controller.loading ? null : [AutofillHints.username],
 | 
			
		||||
                  validator: controller.usernameTextFieldValidator,
 | 
			
		||||
                  decoration: InputDecoration(
 | 
			
		||||
                      prefixIcon: const Icon(Icons.account_circle_outlined),
 | 
			
		||||
                      hintText: L10n.of(context)!.username,
 | 
			
		||||
                      labelText: L10n.of(context)!.username,
 | 
			
		||||
                      prefixText: '@',
 | 
			
		||||
                      prefixStyle: const TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
                      suffixStyle: const TextStyle(fontWeight: FontWeight.w200),
 | 
			
		||||
                      suffixText: ':${controller.domain}'),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(12.0),
 | 
			
		||||
                child: TextFormField(
 | 
			
		||||
                  readOnly: controller.loading,
 | 
			
		||||
                  autocorrect: false,
 | 
			
		||||
                  autofillHints:
 | 
			
		||||
                      controller.loading ? null : [AutofillHints.password],
 | 
			
		||||
                  controller: controller.passwordController,
 | 
			
		||||
                  obscureText: !controller.showPassword,
 | 
			
		||||
                  validator: controller.password1TextFieldValidator,
 | 
			
		||||
                  decoration: InputDecoration(
 | 
			
		||||
                    prefixIcon: const Icon(Icons.vpn_key_outlined),
 | 
			
		||||
                    hintText: '****',
 | 
			
		||||
                    suffixIcon: IconButton(
 | 
			
		||||
                      tooltip: L10n.of(context)!.showPassword,
 | 
			
		||||
                      icon: Icon(controller.showPassword
 | 
			
		||||
                          ? Icons.visibility_off_outlined
 | 
			
		||||
                          : Icons.visibility_outlined),
 | 
			
		||||
                      onPressed: controller.toggleShowPassword,
 | 
			
		||||
                    ),
 | 
			
		||||
                    labelText: L10n.of(context)!.password,
 | 
			
		||||
      ),
 | 
			
		||||
      body: Form(
 | 
			
		||||
        key: controller.formKey,
 | 
			
		||||
        child: ListView(
 | 
			
		||||
          children: [
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.all(16.0),
 | 
			
		||||
              child: TextFormField(
 | 
			
		||||
                readOnly: controller.loading,
 | 
			
		||||
                autocorrect: false,
 | 
			
		||||
                autofillHints:
 | 
			
		||||
                    controller.loading ? null : [AutofillHints.password],
 | 
			
		||||
                controller: controller.passwordController,
 | 
			
		||||
                obscureText: !controller.showPassword,
 | 
			
		||||
                validator: controller.password1TextFieldValidator,
 | 
			
		||||
                decoration: FluffyThemes.loginTextFieldDecoration(
 | 
			
		||||
                  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),
 | 
			
		||||
                    onPressed: controller.toggleShowPassword,
 | 
			
		||||
                  ),
 | 
			
		||||
                  hintText: L10n.of(context)!.chooseAStrongPassword,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(12.0),
 | 
			
		||||
                child: TextFormField(
 | 
			
		||||
                  readOnly: controller.loading,
 | 
			
		||||
                  autocorrect: false,
 | 
			
		||||
                  autofillHints:
 | 
			
		||||
                      controller.loading ? null : [AutofillHints.password],
 | 
			
		||||
                  controller: controller.passwordController2,
 | 
			
		||||
                  obscureText: true,
 | 
			
		||||
                  validator: controller.password2TextFieldValidator,
 | 
			
		||||
                  decoration: InputDecoration(
 | 
			
		||||
                    prefixIcon: const Icon(Icons.repeat_outlined),
 | 
			
		||||
                    hintText: '****',
 | 
			
		||||
                    labelText: L10n.of(context)!.repeatPassword,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              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(
 | 
			
		||||
            ),
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.all(16.0),
 | 
			
		||||
              child: TextFormField(
 | 
			
		||||
                readOnly: controller.loading,
 | 
			
		||||
                autocorrect: false,
 | 
			
		||||
                controller: controller.emailController,
 | 
			
		||||
                keyboardType: TextInputType.emailAddress,
 | 
			
		||||
                autofillHints:
 | 
			
		||||
                    controller.loading ? null : [AutofillHints.username],
 | 
			
		||||
                validator: controller.emailTextFieldValidator,
 | 
			
		||||
                decoration: FluffyThemes.loginTextFieldDecoration(
 | 
			
		||||
                    prefixIcon: const Icon(Icons.mail_outlined),
 | 
			
		||||
                    labelText: L10n.of(context)!.addEmail,
 | 
			
		||||
                    hintText: 'email@example.abc',
 | 
			
		||||
                    hintText: L10n.of(context)!.enterAnEmailAddress,
 | 
			
		||||
                    errorText: controller.error),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            Hero(
 | 
			
		||||
              tag: 'loginButton',
 | 
			
		||||
              child: Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(16),
 | 
			
		||||
                child: ElevatedButton(
 | 
			
		||||
                  onPressed: controller.loading ? () {} : controller.signup,
 | 
			
		||||
                  style: ElevatedButton.styleFrom(
 | 
			
		||||
                    primary: Colors.white.withAlpha(200),
 | 
			
		||||
                    onPrimary: Colors.black,
 | 
			
		||||
                    shadowColor: Colors.white,
 | 
			
		||||
                  ),
 | 
			
		||||
                  child: controller.loading
 | 
			
		||||
                      ? const LinearProgressIndicator()
 | 
			
		||||
                      : Text(L10n.of(context)!.signUp),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              const Divider(),
 | 
			
		||||
              const SizedBox(height: 12),
 | 
			
		||||
              if (controller.error != null) ...[
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
                  child: Text(
 | 
			
		||||
                    controller.error!,
 | 
			
		||||
                    style: const TextStyle(color: Colors.red),
 | 
			
		||||
                    textAlign: TextAlign.center,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const SizedBox(height: 12),
 | 
			
		||||
                const Divider(),
 | 
			
		||||
                const SizedBox(height: 12),
 | 
			
		||||
              ],
 | 
			
		||||
              Hero(
 | 
			
		||||
                tag: 'loginButton',
 | 
			
		||||
                child: Padding(
 | 
			
		||||
                  padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
                  child: ElevatedButton(
 | 
			
		||||
                    onPressed: controller.loading ? null : controller.signup,
 | 
			
		||||
                    child: controller.loading
 | 
			
		||||
                        ? const LinearProgressIndicator()
 | 
			
		||||
                        : Text(L10n.of(context)!.signUp),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -99,7 +99,9 @@ abstract class ClientManager {
 | 
			
		||||
        legacyDatabaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder,
 | 
			
		||||
        supportedLoginTypes: {
 | 
			
		||||
          AuthenticationTypes.password,
 | 
			
		||||
          if (PlatformInfos.isMobile || PlatformInfos.isWeb)
 | 
			
		||||
          if (PlatformInfos.isMobile ||
 | 
			
		||||
              PlatformInfos.isWeb ||
 | 
			
		||||
              PlatformInfos.isMacOS)
 | 
			
		||||
            AuthenticationTypes.sso
 | 
			
		||||
        },
 | 
			
		||||
        compute: compute,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										45
									
								
								lib/widgets/layouts/login_scaffold.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								lib/widgets/layouts/login_scaffold.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
 | 
			
		||||
class LoginScaffold extends StatelessWidget {
 | 
			
		||||
  final Widget body;
 | 
			
		||||
  final AppBar? appBar;
 | 
			
		||||
 | 
			
		||||
  const LoginScaffold({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.body,
 | 
			
		||||
    this.appBar,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    SystemChrome.setSystemUIOverlayStyle(
 | 
			
		||||
      const SystemUiOverlayStyle(
 | 
			
		||||
        statusBarIconBrightness: Brightness.light,
 | 
			
		||||
        statusBarColor: Colors.transparent,
 | 
			
		||||
        systemNavigationBarContrastEnforced: false,
 | 
			
		||||
        systemNavigationBarColor: Colors.transparent,
 | 
			
		||||
        systemNavigationBarIconBrightness: Brightness.light,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: appBar,
 | 
			
		||||
      extendBodyBehindAppBar: true,
 | 
			
		||||
      body: Container(
 | 
			
		||||
        decoration: const BoxDecoration(
 | 
			
		||||
          image: DecorationImage(
 | 
			
		||||
            fit: BoxFit.cover,
 | 
			
		||||
            image: AssetImage(
 | 
			
		||||
              'assets/login_wallpaper.png',
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        alignment: Alignment.center,
 | 
			
		||||
        child: ConstrainedBox(
 | 
			
		||||
          constraints: const BoxConstraints(maxWidth: 480),
 | 
			
		||||
          child: body,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,48 +0,0 @@
 | 
			
		||||
import 'dart:math';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/config/themes.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
 | 
			
		||||
class OnePageCard extends StatelessWidget {
 | 
			
		||||
  final Widget child;
 | 
			
		||||
 | 
			
		||||
  /// This will cause the "isLogged()" check to be skipped and force a
 | 
			
		||||
  /// OnePageCard without login wallpaper. This can be used in situations where
 | 
			
		||||
  /// "Matrix.of(context) is not yet available, e.g. in the LockScreen widget.
 | 
			
		||||
  final bool forceBackgroundless;
 | 
			
		||||
 | 
			
		||||
  const OnePageCard(
 | 
			
		||||
      {Key? key, required this.child, this.forceBackgroundless = false})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
  static const int alpha = 12;
 | 
			
		||||
  static num breakpoint = FluffyThemes.columnWidth * 2;
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final horizontalPadding =
 | 
			
		||||
        max<double>((MediaQuery.of(context).size.width - 600) / 2, 24);
 | 
			
		||||
    return MediaQuery.of(context).size.width <= breakpoint ||
 | 
			
		||||
            forceBackgroundless ||
 | 
			
		||||
            Matrix.of(context).client.isLogged()
 | 
			
		||||
        ? child
 | 
			
		||||
        : Container(
 | 
			
		||||
            decoration: const BoxDecoration(
 | 
			
		||||
              image: DecorationImage(
 | 
			
		||||
                image: AssetImage('assets/login_wallpaper.jpg'),
 | 
			
		||||
                fit: BoxFit.cover,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            child: Padding(
 | 
			
		||||
              padding: EdgeInsets.only(
 | 
			
		||||
                top: 16,
 | 
			
		||||
                left: horizontalPadding,
 | 
			
		||||
                right: horizontalPadding,
 | 
			
		||||
                bottom: max((MediaQuery.of(context).size.height - 600) / 2, 24),
 | 
			
		||||
              ),
 | 
			
		||||
              child: SafeArea(child: Card(elevation: 16, child: child)),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -9,7 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart';
 | 
			
		||||
import 'package:fluffychat/config/app_config.dart';
 | 
			
		||||
import 'package:fluffychat/config/setting_keys.dart';
 | 
			
		||||
import 'package:fluffychat/config/themes.dart';
 | 
			
		||||
import 'layouts/one_page_card.dart';
 | 
			
		||||
import 'layouts/login_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
class LockScreen extends StatefulWidget {
 | 
			
		||||
  const LockScreen({Key? key}) : super(key: key);
 | 
			
		||||
@ -30,62 +30,58 @@ class _LockScreenState extends State<LockScreen> {
 | 
			
		||||
      localizationsDelegates: L10n.localizationsDelegates,
 | 
			
		||||
      supportedLocales: L10n.supportedLocales,
 | 
			
		||||
      home: Builder(
 | 
			
		||||
        builder: (context) => OnePageCard(
 | 
			
		||||
          forceBackgroundless: true,
 | 
			
		||||
          child: Scaffold(
 | 
			
		||||
            appBar: AppBar(
 | 
			
		||||
              automaticallyImplyLeading: false,
 | 
			
		||||
              elevation: 0,
 | 
			
		||||
              centerTitle: true,
 | 
			
		||||
              title: Text(L10n.of(context)!.pleaseEnterYourPin),
 | 
			
		||||
              backgroundColor: Colors.transparent,
 | 
			
		||||
        builder: (context) => LoginScaffold(
 | 
			
		||||
          appBar: AppBar(
 | 
			
		||||
            automaticallyImplyLeading: false,
 | 
			
		||||
            elevation: 0,
 | 
			
		||||
            centerTitle: true,
 | 
			
		||||
            title: Text(L10n.of(context)!.pleaseEnterYourPin),
 | 
			
		||||
            backgroundColor: Colors.transparent,
 | 
			
		||||
          ),
 | 
			
		||||
          body: Container(
 | 
			
		||||
            decoration: BoxDecoration(
 | 
			
		||||
              color: Theme.of(context).backgroundColor,
 | 
			
		||||
              gradient: LinearGradient(
 | 
			
		||||
                begin: Alignment.topRight,
 | 
			
		||||
                end: Alignment.bottomLeft,
 | 
			
		||||
                stops: const [
 | 
			
		||||
                  0.1,
 | 
			
		||||
                  0.4,
 | 
			
		||||
                  0.6,
 | 
			
		||||
                  0.9,
 | 
			
		||||
                ],
 | 
			
		||||
                colors: [
 | 
			
		||||
                  Theme.of(context).secondaryHeaderColor.withAlpha(16),
 | 
			
		||||
                  Theme.of(context).primaryColor.withAlpha(16),
 | 
			
		||||
                  Theme.of(context).colorScheme.secondary.withAlpha(16),
 | 
			
		||||
                  Theme.of(context).backgroundColor.withAlpha(16),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            extendBodyBehindAppBar: true,
 | 
			
		||||
            body: Container(
 | 
			
		||||
              decoration: BoxDecoration(
 | 
			
		||||
                color: Theme.of(context).backgroundColor,
 | 
			
		||||
                gradient: LinearGradient(
 | 
			
		||||
                  begin: Alignment.topRight,
 | 
			
		||||
                  end: Alignment.bottomLeft,
 | 
			
		||||
                  stops: const [
 | 
			
		||||
                    0.1,
 | 
			
		||||
                    0.4,
 | 
			
		||||
                    0.6,
 | 
			
		||||
                    0.9,
 | 
			
		||||
                  ],
 | 
			
		||||
                  colors: [
 | 
			
		||||
                    Theme.of(context).secondaryHeaderColor.withAlpha(16),
 | 
			
		||||
                    Theme.of(context).primaryColor.withAlpha(16),
 | 
			
		||||
                    Theme.of(context).colorScheme.secondary.withAlpha(16),
 | 
			
		||||
                    Theme.of(context).backgroundColor.withAlpha(16),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              alignment: Alignment.center,
 | 
			
		||||
              child: PinCodeTextField(
 | 
			
		||||
                autofocus: true,
 | 
			
		||||
                controller: _textEditingController,
 | 
			
		||||
                focusNode: _focusNode,
 | 
			
		||||
                pinBoxRadius: AppConfig.borderRadius,
 | 
			
		||||
                pinTextStyle: const TextStyle(fontSize: 32),
 | 
			
		||||
                hideCharacter: true,
 | 
			
		||||
                hasError: _wrongInput,
 | 
			
		||||
                onDone: (String input) async {
 | 
			
		||||
                  if (input ==
 | 
			
		||||
                      await ([TargetPlatform.linux]
 | 
			
		||||
                              .contains(Theme.of(context).platform)
 | 
			
		||||
                          ? SharedPreferences.getInstance().then((prefs) =>
 | 
			
		||||
                              prefs.getString(SettingKeys.appLockKey))
 | 
			
		||||
                          : const FlutterSecureStorage()
 | 
			
		||||
                              .read(key: SettingKeys.appLockKey))) {
 | 
			
		||||
                    AppLock.of(context)!.didUnlock();
 | 
			
		||||
                  } else {
 | 
			
		||||
                    _textEditingController.clear();
 | 
			
		||||
                    setState(() => _wrongInput = true);
 | 
			
		||||
                    _focusNode.requestFocus();
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            alignment: Alignment.center,
 | 
			
		||||
            child: PinCodeTextField(
 | 
			
		||||
              autofocus: true,
 | 
			
		||||
              controller: _textEditingController,
 | 
			
		||||
              focusNode: _focusNode,
 | 
			
		||||
              pinBoxRadius: AppConfig.borderRadius,
 | 
			
		||||
              pinTextStyle: const TextStyle(fontSize: 32),
 | 
			
		||||
              hideCharacter: true,
 | 
			
		||||
              hasError: _wrongInput,
 | 
			
		||||
              onDone: (String input) async {
 | 
			
		||||
                if (input ==
 | 
			
		||||
                    await ([TargetPlatform.linux]
 | 
			
		||||
                            .contains(Theme.of(context).platform)
 | 
			
		||||
                        ? SharedPreferences.getInstance().then(
 | 
			
		||||
                            (prefs) => prefs.getString(SettingKeys.appLockKey))
 | 
			
		||||
                        : const FlutterSecureStorage()
 | 
			
		||||
                            .read(key: SettingKeys.appLockKey))) {
 | 
			
		||||
                  AppLock.of(context)!.didUnlock();
 | 
			
		||||
                } else {
 | 
			
		||||
                  _textEditingController.clear();
 | 
			
		||||
                  setState(() => _wrongInput = true);
 | 
			
		||||
                  _focusNode.requestFocus();
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ import 'package:flutter_app_lock/flutter_app_lock.dart';
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
 | 
			
		||||
import 'package:http/http.dart' as http;
 | 
			
		||||
import 'package:image_picker/image_picker.dart';
 | 
			
		||||
import 'package:matrix/encryption.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
@ -71,6 +72,11 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
 | 
			
		||||
  Store store = Store();
 | 
			
		||||
  late BuildContext navigatorContext;
 | 
			
		||||
 | 
			
		||||
  HomeserverSummary? loginHomeserverSummary;
 | 
			
		||||
  XFile? loginAvatar;
 | 
			
		||||
  String? loginUsername;
 | 
			
		||||
  bool? loginRegistrationSupported;
 | 
			
		||||
 | 
			
		||||
  BackgroundPush? _backgroundPush;
 | 
			
		||||
 | 
			
		||||
  Client get client {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user