diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index f91cb8b0e..0b6d669a6 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4631,5 +4631,21 @@ "couldNotFindTTS": "We couldn't find a text-to-speech engine for your current target language. ", "ttsInstructionsHyperlink": "Click here to view instructions for downloading a new voice on your device.", "currentVersion": "Current Version", - "latestVersion": "Latest Version" + "latestVersion": "Latest Version", + "createAnAccount": "Create an account", + "signIn": "Sign in", + "signUpWithEmail": "Sign up with Email", + "signUpWithGoogle": "Sign up with Google", + "signUpWithApple": "Sign up with Apple", + "yourUsername": "Your username", + "yourEmail": "Your email", + "pleaseEnterAnEmail": "Please enter an email address", + "signInWithGoogle": "Sign in with Google", + "signInWithApple": "Sign in with Apple", + "chooseYourAvatar": "Choose your avatar", + "iWantToLearn": "I want to learn", + "letsStart": "Let's start", + "pleaseAgreeToTOS": "Please agree to the Terms and Conditions", + "pleaseEnterEmail": "Please enter a valid email address.", + "pleaseSelectALanguage": "Please select a language" } diff --git a/assets/pangea/Avatar_1.png b/assets/pangea/Avatar_1.png new file mode 100644 index 000000000..6bb59107f Binary files /dev/null and b/assets/pangea/Avatar_1.png differ diff --git a/assets/pangea/Avatar_2.png b/assets/pangea/Avatar_2.png new file mode 100644 index 000000000..78733639e Binary files /dev/null and b/assets/pangea/Avatar_2.png differ diff --git a/assets/pangea/Avatar_3.png b/assets/pangea/Avatar_3.png new file mode 100644 index 000000000..e38807d8c Binary files /dev/null and b/assets/pangea/Avatar_3.png differ diff --git a/assets/pangea/Avatar_4.png b/assets/pangea/Avatar_4.png new file mode 100644 index 000000000..ba23b31d2 Binary files /dev/null and b/assets/pangea/Avatar_4.png differ diff --git a/assets/pangea/Avatar_5.png b/assets/pangea/Avatar_5.png new file mode 100644 index 000000000..5deb9c218 Binary files /dev/null and b/assets/pangea/Avatar_5.png differ diff --git a/assets/pangea/PangeaChat_Glow_Logo.png b/assets/pangea/PangeaChat_Glow_Logo.png new file mode 100644 index 000000000..2256313b1 Binary files /dev/null and b/assets/pangea/PangeaChat_Glow_Logo.png differ diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 49c0af105..b6722a857 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pages/chat_members/chat_members.dart'; import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart'; import 'package:fluffychat/pages/chat_search/chat_search_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'; import 'package:fluffychat/pages/login/login.dart'; import 'package:fluffychat/pages/new_group/new_group.dart'; @@ -28,9 +27,10 @@ import 'package:fluffychat/pages/settings_security/settings_security.dart'; import 'package:fluffychat/pages/settings_style/settings_style.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; import 'package:fluffychat/pangea/pages/find_partner/find_partner.dart'; -import 'package:fluffychat/pangea/pages/p_user_age/p_user_age.dart'; import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart'; +import 'package:fluffychat/pangea/pages/sign_up/login_or_signup_view.dart'; import 'package:fluffychat/pangea/pages/sign_up/signup.dart'; +import 'package:fluffychat/pangea/pages/sign_up/user_settings.dart'; import 'package:fluffychat/pangea/widgets/class/join_with_link.dart'; import 'package:fluffychat/widgets/layouts/empty_page.dart'; import 'package:fluffychat/widgets/layouts/two_column_layout.dart'; @@ -73,7 +73,10 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - const HomeserverPicker(addMultiAccount: false), + // #Pangea + // const HomeserverPicker(addMultiAccount: false), + const LoginOrSignupView(), + // Pangea# ), redirect: loggedInRedirect, routes: [ @@ -95,6 +98,17 @@ abstract class AppRoutes { const SignupPage(), ), redirect: loggedInRedirect, + routes: [ + GoRoute( + path: 'email', + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + const SignupPage(withEmail: true), + ), + redirect: loggedInRedirect, + ), + ], ), // Pangea# ], @@ -121,7 +135,7 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - const PUserAge(), + const UserSettingsPage(), ), redirect: loggedOutRedirect, ), diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 71bb0a4c5..26e3c84bc 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -4,8 +4,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/tor_stub.dart' @@ -13,7 +11,6 @@ import 'package:fluffychat/utils/tor_stub.dart' import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:go_router/go_router.dart'; @@ -36,11 +33,9 @@ class HomeserverPickerController extends State { bool isLoading = false; bool isLoggingIn = false; - // #Pangea - // final TextEditingController homeserverController = TextEditingController( - // text: AppConfig.defaultHomeserver, - // ); - // Pangea# + final TextEditingController homeserverController = TextEditingController( + text: AppConfig.defaultHomeserver, + ); String? error; @@ -82,71 +77,48 @@ class HomeserverPickerController extends State { checkHomeserverAction(); } - // #Pangea - Map? _rawLoginTypes; - // Pangea# - - // #Pangea - // void onSubmitted([_]) { - // if (isLoading || _checkHomeserverCooldown?.isActive == true) { - // return tryCheckHomeserverActionWithoutCooldown(); - // } - // if (supportsSso) return ssoLoginAction(); - // if (supportsPasswordLogin) return login(); - // return tryCheckHomeserverActionWithoutCooldown(); - // } - // Pangea# + void onSubmitted([_]) { + if (isLoading || _checkHomeserverCooldown?.isActive == true) { + return tryCheckHomeserverActionWithoutCooldown(); + } + if (supportsSso) return ssoLoginAction(); + if (supportsPasswordLogin) return login(); + return tryCheckHomeserverActionWithoutCooldown(); + } /// 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 checkHomeserverAction([_]) async { - // #Pangea - // final homeserverInput = - // homeserverController.text.trim().toLowerCase().replaceAll(' ', '-'); + final homeserverInput = + homeserverController.text.trim().toLowerCase().replaceAll(' ', '-'); - // if (homeserverInput.isEmpty || !homeserverInput.contains('.')) { - // setState(() { - // error = loginFlows = null; - // isLoading = false; - // Matrix.of(context).getLoginClient().homeserver = null; - // _lastCheckedUrl = null; - // }); - // return; - // } - // if (_lastCheckedUrl == homeserverInput) return; + if (homeserverInput.isEmpty || !homeserverInput.contains('.')) { + setState(() { + error = loginFlows = null; + isLoading = false; + Matrix.of(context).getLoginClient().homeserver = null; + _lastCheckedUrl = null; + }); + return; + } + if (_lastCheckedUrl == homeserverInput) return; - // _lastCheckedUrl = homeserverInput; - _lastCheckedUrl = AppConfig.defaultHomeserver; - // Pangea# + _lastCheckedUrl = homeserverInput; setState(() { error = loginFlows = null; isLoading = true; }); try { - // #Pangea - // var homeserver = Uri.parse(homeserverInput); - // if (homeserver.scheme.isEmpty) { - // homeserver = Uri.https(homeserverInput, ''); - // } - var homeserver = Uri.parse(AppConfig.defaultHomeserver); + var homeserver = Uri.parse(homeserverInput); if (homeserver.scheme.isEmpty) { - homeserver = Uri.https(AppConfig.defaultHomeserver, ''); + homeserver = Uri.https(homeserverInput, ''); } - // Pangea# final client = Matrix.of(context).getLoginClient(); final (_, _, loginFlows) = await client.checkHomeserver(homeserver); this.loginFlows = loginFlows; - // #Pangea - if (supportsSso) { - _rawLoginTypes = await client.request( - RequestType.GET, - '/client/v3/login', - ); - } - // Pangea# } catch (e) { setState( () => error = (e).toLocalizedString( @@ -173,11 +145,7 @@ class HomeserverPickerController extends State { bool get supportsPasswordLogin => _supportsFlow('m.login.password'); - void ssoLoginAction( - // #Pangea - IdentityProvider provider, - // Pangea# - ) async { + void ssoLoginAction() async { final redirectUrl = kIsWeb ? Uri.parse(html.window.location.href) .resolveUri( @@ -189,42 +157,18 @@ class HomeserverPickerController extends State { : 'http://localhost:3001//login'; final url = Matrix.of(context).getLoginClient().homeserver!.replace( - // #Pangea - // path: '/_matrix/client/v3/login/sso/redirect', - path: - '/_matrix/client/v3/login/sso/redirect${provider.id == null ? '' : '/${provider.id}'}', - // Pangea# + path: '/_matrix/client/v3/login/sso/redirect', queryParameters: {'redirectUrl': redirectUrl}, ); final urlScheme = isDefaultPlatform ? Uri.parse(redirectUrl).scheme : "http://localhost:3001"; - // #Pangea - // final result = await FlutterWebAuth2.authenticate( - // url: url.toString(), - // callbackUrlScheme: urlScheme, - // options: const FlutterWebAuth2Options(), - // ); - String result; - try { - result = await FlutterWebAuth2.authenticate( - url: url.toString(), - callbackUrlScheme: urlScheme, - options: const FlutterWebAuth2Options(), - ); - } catch (err) { - if (err is PlatformException && err.code == 'CANCELED') { - debugPrint("user cancelled SSO login"); - return; - } - ErrorHandler.logError( - e: err, - s: StackTrace.current, - ); - return; - } - // Pangea# + final result = await FlutterWebAuth2.authenticate( + url: url.toString(), + callbackUrlScheme: urlScheme, + options: const FlutterWebAuth2Options(), + ); final token = Uri.parse(result).queryParameters['loginToken']; if (token?.isEmpty ?? false) return; @@ -233,17 +177,11 @@ class HomeserverPickerController extends State { isLoading = isLoggingIn = true; }); try { - // #Pangea - final loginRes = await Matrix.of(context).getLoginClient().login( - // await Matrix.of(context).getLoginClient().login( - // Pangea# + await Matrix.of(context).getLoginClient().login( LoginType.mLoginToken, token: token, initialDeviceDisplayName: PlatformInfos.clientName, ); - // #Pangea - GoogleAnalytics.login(provider.name!, loginRes.userId); - // Pangea# } catch (e) { setState(() { error = e.toLocalizedString(context); @@ -258,12 +196,10 @@ class HomeserverPickerController extends State { } void login() async { - // #Pangea - // if (!supportsPasswordLogin) { - // homeserverController.text = AppConfig.defaultHomeserver; - // await checkHomeserverAction(); - // } - // Pangea# + if (!supportsPasswordLogin) { + homeserverController.text = AppConfig.defaultHomeserver; + await checkHomeserverAction(); + } context.push( '${GoRouter.of(context).routeInformationProvider.value.uri.path}/login', ); @@ -291,13 +227,10 @@ class HomeserverPickerController extends State { final client = Matrix.of(context).getLoginClient(); await client.importDump(String.fromCharCodes(await file.readAsBytes())); Matrix.of(context).initMatrix(); - } catch (e, s) { + } catch (e) { setState(() { error = e.toLocalizedString(context); }); - // #Pangea - ErrorHandler.logError(e: e, s: s); - // Pangea# } finally { if (mounted) { setState(() { @@ -307,27 +240,6 @@ class HomeserverPickerController extends State { } } - // #Pangea - List? get identityProviders { - final loginTypes = _rawLoginTypes; - if (loginTypes == null) return null; - final List? rawProviders = - loginTypes.tryGetList('flows')?.singleWhereOrNull( - (flow) => flow['type'] == AuthenticationTypes.sso, - )['identity_providers'] ?? - [ - {'id': null}, - ]; - if (rawProviders == null) return null; - final list = - rawProviders.map((json) => IdentityProvider.fromJson(json)).toList(); - if (PlatformInfos.isCupertinoStyle) { - list.sort((a, b) => a.brand == 'apple' ? -1 : 1); - } - return list; - } - // Pangea# - void onMoreAction(MoreLoginActions action) { switch (action) { case MoreLoginActions.passwordLogin: diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index f5180d31b..7e8541611 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -1,12 +1,14 @@ import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/pages/connect/p_sso_button.dart'; -import 'package:fluffychat/pangea/widgets/common/pangea_logo_svg.dart'; -import 'package:fluffychat/pangea/widgets/signup/signup_buttons.dart'; +import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import '../../config/themes.dart'; import 'homeserver_picker.dart'; class HomeserverPickerView extends StatelessWidget { @@ -23,306 +25,225 @@ class HomeserverPickerView extends StatelessWidget { return LoginScaffold( enforceMobileMode: Matrix.of(context).client.isLogged(), - // #Pangea appBar: AppBar( centerTitle: true, title: Text( - AppConfig.applicationName, + controller.widget.addMultiAccount + ? L10n.of(context).addAccount + : L10n.of(context).login, ), + actions: [ + PopupMenuButton( + onSelected: controller.onMoreAction, + itemBuilder: (_) => [ + PopupMenuItem( + value: MoreLoginActions.passwordLogin, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.login_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).loginWithMatrixId), + ], + ), + ), + PopupMenuItem( + value: MoreLoginActions.privacy, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.privacy_tip_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).privacy), + ], + ), + ), + PopupMenuItem( + value: MoreLoginActions.about, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).about), + ], + ), + ), + ], + ), + ], ), - // appBar: AppBar( - // centerTitle: true, - // title: Text( - // controller.widget.addMultiAccount - // ? L10n.of(context).addAccount - // : L10n.of(context).login, - // ), - // actions: [ - // PopupMenuButton( - // onSelected: controller.onMoreAction, - // itemBuilder: (_) => [ - // PopupMenuItem( - // value: MoreLoginActions.passwordLogin, - // child: Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // const Icon(Icons.login_outlined), - // const SizedBox(width: 12), - // Text(L10n.of(context).loginWithMatrixId), - // ], - // ), - // ), - // PopupMenuItem( - // value: MoreLoginActions.privacy, - // child: Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // const Icon(Icons.privacy_tip_outlined), - // const SizedBox(width: 12), - // Text(L10n.of(context).privacy), - // ], - // ), - // ), - // PopupMenuItem( - // value: MoreLoginActions.about, - // child: Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // const Icon(Icons.info_outlined), - // const SizedBox(width: 12), - // Text(L10n.of(context).about), - // ], - // ), - // ), - // ], - // ), - // ], - // ), - // Pangea# body: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: IntrinsicHeight( - child: controller.isLoading - ? const Center( - child: CircularProgressIndicator(), - ) - : Column( + child: Column( + children: [ + // display a prominent banner to import session for TOR browser + // users. This feature is just some UX sugar as TOR users are + // usually forced to logout as TOR browser is non-persistent + AnimatedContainer( + height: controller.isTorBrowser ? 64 : 0, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: Material( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(8), + ), + color: theme.colorScheme.surface, + child: ListTile( + leading: const Icon(Icons.vpn_key), + title: Text(L10n.of(context).hydrateTor), + subtitle: Text(L10n.of(context).hydrateTorLong), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: controller.restoreBackup, + ), + ), + ), + Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Hero( + tag: 'info-logo', + child: Image.asset( + './assets/banner_transparent.png', + fit: BoxFit.fitWidth, + ), + ), + ), + const SizedBox(height: 32), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: SelectableLinkify( + text: L10n.of(context).welcomeText, + style: TextStyle( + color: theme.colorScheme.onSecondaryContainer, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + linkStyle: TextStyle( + color: theme.colorScheme.secondary, + decorationColor: theme.colorScheme.secondary, + ), + onOpen: (link) => launchUrlString(link.url), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (controller.error != null) ...[ - const SizedBox(height: 12), - const Center( - child: Icon( - Icons.error_outline, - size: 48, - color: Colors.orange, - ), - ), - const SizedBox(height: 12), - Center( - child: Text( - controller.error!, - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - fontSize: 18, + TextField( + onChanged: + controller.tryCheckHomeserverActionWithCooldown, + onSubmitted: controller.onSubmitted, + onTap: + controller.tryCheckHomeserverActionWithCooldown, + controller: controller.homeserverController, + autocorrect: false, + keyboardType: TextInputType.url, + decoration: InputDecoration( + prefixIcon: controller.isLoading + ? Container( + width: 16, + height: 16, + alignment: Alignment.center, + child: const SizedBox( + width: 16, + height: 16, + child: + CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ) + : const Icon(Icons.search_outlined), + filled: false, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, ), ), - ), - const SizedBox(height: 36), - ] else - const SignupButtons(), - if (controller.identityProviders != null) ...[ - ...controller.identityProviders!.map( - (provider) => Padding( - padding: const EdgeInsets.all(12.0), - child: Hero( - tag: - "ssobutton ${provider.id ?? provider.name}", - child: PangeaSsoButton( - identityProvider: provider, - onPressed: () => - controller.ssoLoginAction(provider), - ), - ), + hintText: AppConfig.defaultHomeserver, + hintStyle: TextStyle( + color: theme.colorScheme.surfaceTint, ), - ), - if (controller.supportsPasswordLogin) - Padding( - padding: const EdgeInsets.all(12.0), - child: Hero( - tag: 'signinButton', - child: ElevatedButton( - onPressed: controller.login, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const PangeaLogoSvg(width: 20), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, + labelText: 'Sign in with:', + errorText: controller.error, + errorMaxLines: 4, + suffixIcon: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog.adaptive( + title: Text( + L10n.of(context).whatIsAHomeserver, + ), + content: Linkify( + text: L10n.of(context) + .homeserverDescription, + ), + actions: [ + AdaptiveDialogAction( + onPressed: () => launchUrl( + Uri.https('servers.joinmatrix.org'), ), child: Text( - L10n.of(context).signInWithUsername, + L10n.of(context) + .discoverHomeservers, ), ), + AdaptiveDialogAction( + onPressed: Navigator.of(context).pop, + child: Text(L10n.of(context).close), + ), ], ), - ), - ), + ); + }, + icon: const Icon(Icons.info_outlined), ), - ], - // display a prominent banner to import session for TOR browser - // users. This feature is just some UX sugar as TOR users are - // usually forced to logout as TOR browser is non-persistent - // #Pangea - // AnimatedContainer( - // height: controller.isTorBrowser ? 64 : 0, - // duration: FluffyThemes.animationDuration, - // curve: FluffyThemes.animationCurve, - // clipBehavior: Clip.hardEdge, - // decoration: const BoxDecoration(), - // child: Material( - // clipBehavior: Clip.hardEdge, - // borderRadius: const BorderRadius.vertical( - // bottom: Radius.circular(8), - // ), - // color: theme.colorScheme.surface, - // child: ListTile( - // leading: const Icon(Icons.vpn_key), - // title: Text(L10n.of(context).hydrateTor), - // subtitle: Text(L10n.of(context).hydrateTorLong), - // trailing: const Icon(Icons.chevron_right_outlined), - // onTap: controller.restoreBackup, - // ), - // ), - // ), - // Container( - // alignment: Alignment.center, - // padding: const EdgeInsets.symmetric(horizontal: 8.0), - // child: Hero( - // tag: 'info-logo', - // child: Image.asset( - // './assets/banner_transparent.png', - // fit: BoxFit.fitWidth, - // ), - // ), - // ), - // const SizedBox(height: 32), - // Padding( - // padding: const EdgeInsets.symmetric(horizontal: 32.0), - // child: SelectableLinkify( - // text: L10n.of(context).welcomeText, - // style: TextStyle( - // color: theme.colorScheme.onSecondaryContainer, - // fontWeight: FontWeight.w500, - // ), - // textAlign: TextAlign.center, - // linkStyle: TextStyle( - // color: theme.colorScheme.secondary, - // decorationColor: theme.colorScheme.secondary, - // ), - // onOpen: (link) => launchUrlString(link.url), - // ), - // ), - // const Spacer(), - // const Padding( - // padding: EdgeInsets.all(32.0), - // child: Column( - // mainAxisSize: MainAxisSize.min, - // crossAxisAlignment: CrossAxisAlignment.stretch, - // children: [ - // TextField( - // onChanged: - // controller.tryCheckHomeserverActionWithCooldown, - // onEditingComplete: controller - // .tryCheckHomeserverActionWithoutCooldown, - // onSubmitted: controller - // .tryCheckHomeserverActionWithoutCooldown, - // onTap: - // controller.tryCheckHomeserverActionWithCooldown, - // controller: controller.homeserverController, - // autocorrect: false, - // keyboardType: TextInputType.url, - // decoration: InputDecoration( - // prefixIcon: controller.isLoading - // ? Container( - // width: 16, - // height: 16, - // alignment: Alignment.center, - // child: const SizedBox( - // width: 16, - // height: 16, - // child: - // CircularProgressIndicator.adaptive( - // strokeWidth: 2, - // ), - // ), - // ) - // : const Icon(Icons.search_outlined), - // filled: false, - // border: OutlineInputBorder( - // borderRadius: BorderRadius.circular( - // AppConfig.borderRadius, - // ), - // ), - // hintText: AppConfig.defaultHomeserver, - // hintStyle: TextStyle( - // color: theme.colorScheme.surfaceTint, - // ), - // labelText: 'Sign in with:', - // errorText: controller.error, - // errorMaxLines: 4, - // suffixIcon: IconButton( - // onPressed: () { - // showDialog( - // context: context, - // builder: (context) => AlertDialog.adaptive( - // title: Text( - // L10n.of(context).whatIsAHomeserver, - // ), - // content: Linkify( - // text: L10n.of(context) - // .homeserverDescription, - // ), - // actions: [ - // AdaptiveDialogAction( - // onPressed: () => launchUrl( - // Uri.https('servers.joinmatrix.org'), - // ), - // child: Text( - // L10n.of(context) - // .discoverHomeservers, - // ), - // ), - // AdaptiveDialogAction( - // onPressed: Navigator.of(context).pop, - // child: Text(L10n.of(context).close), - // ), - // ], - // ), - // ); - // }, - // icon: const Icon(Icons.info_outlined), - // ), - // ), - // ), - // const SizedBox(height: 32), - // ElevatedButton( - // style: ElevatedButton.styleFrom( - // backgroundColor: theme.colorScheme.primary, - // foregroundColor: theme.colorScheme.onPrimary, - // ), - // onPressed: - // controller.isLoggingIn || controller.isLoading - // ? null - // : () => controller.supportsSso - // ? controller.ssoLoginAction - // : controller.supportsPasswordLogin - // ? controller.login - // : null, - // child: Text(L10n.of(context).continueText), - // ), - // TextButton( - // style: TextButton.styleFrom( - // foregroundColor: theme.colorScheme.secondary, - // textStyle: theme.textTheme.labelMedium, - // ), - // onPressed: - // controller.isLoggingIn || controller.isLoading - // ? null - // : controller.restoreBackup, - // child: Text(L10n.of(context).hydrate), - // ), - // Pangea# - // ], - // ), - // ), + ), + ), + const SizedBox(height: 32), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + ), + onPressed: + controller.isLoggingIn || controller.isLoading + ? null + : controller.supportsSso + ? controller.ssoLoginAction + : controller.supportsPasswordLogin + ? controller.login + : null, + child: Text(L10n.of(context).continueText), + ), + TextButton( + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.secondary, + textStyle: theme.textTheme.labelMedium, + ), + onPressed: + controller.isLoggingIn || controller.isLoading + ? null + : controller.restoreBackup, + child: Text(L10n.of(context).hydrate), + ), ], ), + ), + ], + ), ), ), ); diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart index 68f90c2e4..05bf039fe 100644 --- a/lib/pages/login/login.dart +++ b/lib/pages/login/login.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/pages/sign_up/pangea_login_view.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -11,7 +12,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import '../../utils/platform_infos.dart'; -import 'login_view.dart'; class Login extends StatefulWidget { const Login({super.key}); @@ -23,13 +23,26 @@ class Login extends StatefulWidget { class LoginController extends State { final TextEditingController usernameController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); + String? usernameText; + String? passwordText; + String? usernameError; String? passwordError; + bool loading = false; bool showPassword = false; // #Pangea final PangeaController pangeaController = MatrixState.pangeaController; + final GlobalKey formKey = GlobalKey(); + + bool get enabledSignIn => + !loading && + usernameText != null && + usernameText!.isNotEmpty && + passwordText != null && + passwordText!.isNotEmpty; + @override void initState() { // TODO: implement initState @@ -46,6 +59,25 @@ class LoginController extends State { passwordError = err.toLocalizedString(context); }); }); + + usernameController.addListener(() { + _setStateOnTextChange(usernameText, usernameController.text); + usernameText = usernameController.text; + }); + + passwordController.addListener(() { + _setStateOnTextChange(passwordText, passwordController.text); + passwordText = passwordController.text; + }); + } + + void _setStateOnTextChange(String? oldText, String newText) { + if ((oldText == null || oldText.isEmpty) && (newText.isNotEmpty)) { + setState(() {}); + } + if ((oldText != null && oldText.isNotEmpty) && (newText.isEmpty)) { + setState(() {}); + } } // Pangea# @@ -53,6 +85,11 @@ class LoginController extends State { setState(() => showPassword = !loading && !showPassword); void login() async { + // #Pangea + final valid = formKey.currentState!.validate(); + if (!valid) return; + // Pangea# + final matrix = Matrix.of(context); if (usernameController.text.isEmpty) { setState(() => usernameError = L10n.of(context).pleaseEnterYourUsername); @@ -113,10 +150,22 @@ class LoginController extends State { GoogleAnalytics.login("pangea", loginRes.userId); // Pangea# } on MatrixException catch (exception) { - setState(() => passwordError = exception.errorMessage); + // #Pangea + // setState(() => passwordError = exception.errorMessage); + setState(() { + passwordError = exception.errorMessage; + usernameError = exception.errorMessage; + }); + // Pangea# return setState(() => loading = false); } catch (exception) { - setState(() => passwordError = exception.toString()); + // #Pangea + // setState(() => passwordError = exception.toString()); + setState(() { + passwordError = exception.toString(); + usernameError = exception.toString(); + }); + // Pangea# return setState(() => loading = false); } @@ -279,7 +328,10 @@ class LoginController extends State { static int sendAttempt = 0; @override - Widget build(BuildContext context) => LoginView(this); + // #Pangea + // Widget build(BuildContext context) => LoginView(this); + Widget build(BuildContext context) => PangeaLoginView(this); + // Pangea# } extension on String { diff --git a/lib/pangea/controllers/permissions_controller.dart b/lib/pangea/controllers/permissions_controller.dart index 3a8eca775..8b06bc773 100644 --- a/lib/pangea/controllers/permissions_controller.dart +++ b/lib/pangea/controllers/permissions_controller.dart @@ -76,12 +76,12 @@ class PermissionsController extends BaseController { // _getRoomRules(roomID)?.isShareVideo ?? isUser18(); /// works for both roomID of chat and class - bool canSharePhoto(String? roomID) => isUser18(); + bool canSharePhoto(String? roomID) => true; // Rules can't be edited; default to true // _getRoomRules(roomID)?.isSharePhoto ?? isUser18(); /// works for both roomID of chat and class - bool canShareFile(String? roomID) => isUser18(); + bool canShareFile(String? roomID) => true; // Rules can't be edited; default to true // _getRoomRules(roomID)?.isShareFiles ?? isUser18(); diff --git a/lib/pangea/controllers/user_controller.dart b/lib/pangea/controllers/user_controller.dart index c076063c3..4251bc558 100644 --- a/lib/pangea/controllers/user_controller.dart +++ b/lib/pangea/controllers/user_controller.dart @@ -84,7 +84,7 @@ class UserController extends BaseController { } /// Creates a new profile for the user with the given date of birth. - Future createProfile({required DateTime dob}) async { + Future createProfile({DateTime? dob}) async { final userSettings = UserSettings( dateOfBirth: dob, createdAt: DateTime.now(), @@ -165,15 +165,13 @@ class UserController extends BaseController { return userId!.substring(0, userId!.indexOf(":")).replaceAll("@", ""); } - /// Checks if user data is available and the date of birth is set. - /// Returns a [Future] that completes with a [bool] value indicating - /// whether the user data is available and the date of birth is set. - Future get isUserDataAvailableAndDateOfBirthSet async { + /// Checks if user data is available and the user's l2 is set. + Future get isUserDataAvailableAndL2Set async { try { // the function fetchUserModel() uses a completer, so it shouldn't // re-call the endpoint if it has already been called await initialize(); - return profile.userSettings.dateOfBirth != null; + return profile.userSettings.targetLanguage != null; } catch (err, s) { ErrorHandler.logError(e: err, s: s); return false; diff --git a/lib/pangea/guard/p_vguard.dart b/lib/pangea/guard/p_vguard.dart index ce3805a63..acc1eb35f 100644 --- a/lib/pangea/guard/p_vguard.dart +++ b/lib/pangea/guard/p_vguard.dart @@ -16,8 +16,8 @@ class PAuthGaurd { ) async { if (pController != null) { if (Matrix.of(context).client.isLogged()) { - final bool dobIsSet = await pController! - .userController.isUserDataAvailableAndDateOfBirthSet; + final bool dobIsSet = + await pController!.userController.isUserDataAvailableAndL2Set; return dobIsSet ? '/rooms' : '/user_age'; } return null; @@ -36,8 +36,8 @@ class PAuthGaurd { if (!Matrix.of(context).client.isLogged()) { return '/home'; } - final bool dobIsSet = await pController! - .userController.isUserDataAvailableAndDateOfBirthSet; + final bool dobIsSet = + await pController!.userController.isUserDataAvailableAndL2Set; return dobIsSet ? null : '/user_age'; } else { debugPrint("controller is null in pguard check"); diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart index cf4e77b51..6d5d37108 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -34,7 +34,9 @@ class UserSettings { }); factory UserSettings.fromJson(Map json) => UserSettings( - dateOfBirth: DateTime.parse(json[ModelKey.userDateOfBirth]), + dateOfBirth: json[ModelKey.userDateOfBirth] != null + ? DateTime.parse(json[ModelKey.userDateOfBirth]) + : null, createdAt: json[ModelKey.userCreatedAt] != null ? DateTime.parse(json[ModelKey.userCreatedAt]) : null, diff --git a/lib/pangea/pages/connect/p_sso_button.dart b/lib/pangea/pages/connect/p_sso_button.dart index 146f794d7..b3b6c50a7 100644 --- a/lib/pangea/pages/connect/p_sso_button.dart +++ b/lib/pangea/pages/connect/p_sso_button.dart @@ -1,84 +1,111 @@ -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; +import 'package:fluffychat/pangea/pages/sign_up/full_width_button.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/sso_login_action.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:matrix/matrix.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:matrix/matrix_api_lite/model/matrix_exception.dart'; -class ButtonInfo { - String iconPath; - String text; +enum SSOProvider { google, apple } - ButtonInfo(this.iconPath, this.text); +extension on SSOProvider { + String get id { + switch (this) { + case SSOProvider.google: + return "oidc-google"; + case SSOProvider.apple: + return "oidc-apple"; + } + } + + String get name { + switch (this) { + case SSOProvider.google: + return "Google"; + case SSOProvider.apple: + return "Apple"; + } + } + + String get asset { + switch (this) { + case SSOProvider.google: + return "assets/pangea/google.svg"; + case SSOProvider.apple: + return "assets/pangea/apple.svg"; + } + } } -class PangeaSsoButton extends StatelessWidget { - final IdentityProvider identityProvider; - final void Function()? onPressed; +class PangeaSsoButton extends StatefulWidget { + final String title; + final SSOProvider provider; const PangeaSsoButton({ + required this.title, + required this.provider, super.key, - required this.identityProvider, - this.onPressed, }); - ButtonInfo getButtonInfo(BuildContext context) { - switch (identityProvider.id) { - case "oidc-google": - return ButtonInfo( - "assets/pangea/google.svg", - "${L10n.of(context).loginOrSignup} Google", - ); - case "oidc-apple": - return ButtonInfo( - "assets/pangea/apple.svg", - "${L10n.of(context).loginOrSignup} Apple", - ); - default: - return ButtonInfo( - "assets/pangea/pangea.svg", - "${L10n.of(context).loginOrSignup} Pangea Chat", - ); + @override + PangeaSsoButtonState createState() => PangeaSsoButtonState(); +} + +class PangeaSsoButtonState extends State { + bool _loading = false; + String? _error; + + Future _runSSOLogin() async { + try { + setState(() { + _loading = true; + _error = null; + }); + await pangeaSSOLoginAction( + IdentityProvider( + id: widget.provider.id, + name: widget.provider.name, + ), + Matrix.of(context).getLoginClient(), + context, + ); + } catch (err, s) { + ErrorHandler.logError(e: err, s: s); + if (err is MatrixException) { + _error = err.errorMessage; + } else { + _error = L10n.of(context).oopsSomethingWentWrong; + } + _error = err.toString(); + } finally { + if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { - final ButtonInfo buttonInfo = getButtonInfo(context); - return ElevatedButton( - onPressed: onPressed, - child: Row( + return FullWidthButton( + depressed: _loading, + error: _error, + loading: _loading, + title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - identityProvider.icon == null - ? SvgPicture.asset( - buttonInfo.iconPath, - height: 20, - width: 20, - color: Theme.of(context).brightness == Brightness.light - ? AppConfig.primaryColor - : AppConfig.primaryColorLight, - ) - : Image.network( - Uri.parse(identityProvider.icon!) - .getDownloadLink(Matrix.of(context).getLoginClient()) - .toString(), - width: 32, - height: 32, - ), - // #Pangea - Padding( - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - child: Text( - identityProvider.name != null - ? buttonInfo.text - : (identityProvider.brand != null - ? L10n.of(context).loginOrSignup - : L10n.of(context).loginOrSignup), + SvgPicture.asset( + widget.provider.asset, + height: 20, + width: 20, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onPrimary, + BlendMode.srcIn, ), ), + const SizedBox(width: 10), + Text(widget.title), ], ), + onPressed: _runSSOLogin, ); } } diff --git a/lib/pangea/pages/p_user_age/p_user_age.dart b/lib/pangea/pages/p_user_age/p_user_age.dart deleted file mode 100644 index ce5d55da5..000000000 --- a/lib/pangea/pages/p_user_age/p_user_age.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'dart:developer'; - -import 'package:fluffychat/pangea/constants/age_limits.dart'; -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/pages/p_user_age/p_user_age_view.dart'; -import 'package:fluffychat/pangea/utils/p_extension.dart'; -import 'package:fluffychat/widgets/fluffy_chat_app.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import '../../utils/error_handler.dart'; - -class PUserAge extends StatefulWidget { - const PUserAge({super.key}); - - @override - PUserAgeController createState() => PUserAgeController(); -} - -class PUserAgeController extends State { - bool loading = false; - int? selectedAge; - TextEditingController dobController = TextEditingController(); - - String? error; - bool unknownErrorState = false; - - final PangeaController pangeaController = MatrixState.pangeaController; - - @override - void initState() { - super.initState(); - pangeaController.startChatWithBotIfNotPresent(); - } - - String? dobValidator() { - try { - if (selectedDate == null) { - return L10n.of(context).yourBirthdayPleaseShort; - } - if (!selectedDate!.isAtLeastYearsOld(AgeLimits.toUseTheApp)) { - return L10n.of(context).mustBe13; - } - return null; - } catch (err, stack) { - ErrorHandler.logError(e: err, s: stack); - return L10n.of(context).invalidDob; - } - } - - DateTime? get selectedDate { - if (selectedAge == null) return null; - final now = DateTime.now(); - return DateTime(now.year - selectedAge!, now.month, now.day); - } - - //Note: used linear progress bar (also used in fluffychat signup button) for consistency - Future createUserInPangea() async { - try { - setState(() => error = dobValidator()); - if (error?.isNotEmpty == true) return; - setState(() => loading = true); - - final DateTime? dob = - pangeaController.userController.profile.userSettings.dateOfBirth; - - if (dob == null) { - await pangeaController.userController.createProfile( - dob: selectedDate!, - ); - } else { - pangeaController.userController.updateProfile((profile) { - profile.userSettings.dateOfBirth = selectedDate!; - return profile; - }); - } - pangeaController.subscriptionController.reinitialize(); - FluffyChatApp.router.go('/rooms'); - } catch (err, s) { - setState(() { - unknownErrorState = true; - }); - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s); - } finally { - loading = false; - } - } - - void setSelectedAge(int? value) { - setState(() { - selectedAge = value; - }); - } - - @override - Widget build(BuildContext context) { - return !unknownErrorState - ? PUserAgeView(this) - : Center( - child: Padding( - padding: const EdgeInsets.all(50), - child: Text( - "${L10n.of(context).oopsSomethingWentWrong} \n ${L10n.of(context).errorPleaseRefresh}", - ), - ), - ); - } -} diff --git a/lib/pangea/pages/p_user_age/p_user_age_view.dart b/lib/pangea/pages/p_user_age/p_user_age_view.dart deleted file mode 100644 index 904da2923..000000000 --- a/lib/pangea/pages/p_user_age/p_user_age_view.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/pages/p_user_age/p_user_age.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import '../../../widgets/layouts/login_scaffold.dart'; - -class PUserAgeView extends StatelessWidget { - final PUserAgeController controller; - const PUserAgeView(this.controller, {super.key}); - - @override - Widget build(BuildContext context) { - return LoginScaffold( - appBar: AppBar( - automaticallyImplyLeading: !controller.loading, - ), - body: ListView( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme.of(context) - .colorScheme - .onSecondaryContainer - .withAlpha(50), - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(15), - child: Text( - L10n.of(context).yourBirthdayPlease, - textAlign: TextAlign.justify, - style: const TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.normal, - ), - ), - ), - ListTile( - title: Text( - L10n.of(context).certifyAge(13), - style: const TextStyle(color: Colors.black, fontSize: 14), - ), - leading: Radio( - value: 13, - groupValue: controller.selectedAge, - onChanged: controller.setSelectedAge, - activeColor: AppConfig.primaryColor, - ), - ), - ListTile( - title: Text( - L10n.of(context).certifyAge(18), - style: const TextStyle(color: Colors.black, fontSize: 14), - ), - leading: Radio( - value: 18, - groupValue: controller.selectedAge, - onChanged: controller.setSelectedAge, - activeColor: AppConfig.primaryColor, - ), - ), - ], - ), - ), - const SizedBox(height: 20), - Hero( - tag: 'loginButton', - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: ElevatedButton( - onPressed: controller.createUserInPangea, - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(50), - ), - child: controller.loading - ? const LinearProgressIndicator() - : Text(L10n.of(context).getStarted), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pangea/pages/sign_up/full_width_button.dart b/lib/pangea/pages/sign_up/full_width_button.dart new file mode 100644 index 000000000..b7ef42a0c --- /dev/null +++ b/lib/pangea/pages/sign_up/full_width_button.dart @@ -0,0 +1,146 @@ +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/widgets/pressable_button.dart'; +import 'package:flutter/material.dart'; + +class FullWidthButton extends StatefulWidget { + final Widget title; + final void Function()? onPressed; + final bool depressed; + final String? error; + final bool loading; + final bool enabled; + + const FullWidthButton({ + required this.title, + required this.onPressed, + this.depressed = false, + this.error, + this.loading = false, + this.enabled = true, + super.key, + }); + + @override + FullWidthButtonState createState() => FullWidthButtonState(); +} + +class FullWidthButtonState extends State { + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(4, 4, 4, widget.error == null ? 4 : 0), + child: AnimatedOpacity( + duration: FluffyThemes.animationDuration, + opacity: widget.enabled ? 1 : 0.5, + child: PressableButton( + depressed: widget.depressed || !widget.enabled, + onPressed: widget.onPressed, + borderRadius: BorderRadius.circular(36), + color: Theme.of(context).colorScheme.primary, + child: Builder( + builder: (context) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: widget.enabled + ? Theme.of(context).colorScheme.primary + : Theme.of(context).disabledColor, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + disabledForegroundColor: + Theme.of(context).colorScheme.onPrimary, + textStyle: const TextStyle(fontSize: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(36), + ), + ), + onPressed: widget.enabled + ? () => ButtonPressedNotification().dispatch(context) + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.loading + ? const Expanded(child: LinearProgressIndicator()) + : widget.title, + ], + ), + ); + }, + ), + ), + ), + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: widget.error == null + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30, + vertical: 5, + ), + child: Text( + widget.error!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ), + ), + ], + ); + } +} + +class FullWidthTextField extends StatelessWidget { + final String hintText; + final bool autocorrect; + final bool autofocus; + final bool obscureText; + final TextInputAction? textInputAction; + final TextInputType? keyboardType; + final String? Function(String?)? validator; + final TextEditingController? controller; + final String? errorText; + + const FullWidthTextField({ + required this.hintText, + this.autocorrect = false, + this.autofocus = false, + this.obscureText = false, + this.textInputAction, + this.keyboardType, + this.validator, + this.controller, + this.errorText, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(4.0), + child: TextFormField( + obscureText: obscureText, + autocorrect: autocorrect, + autofocus: autofocus, + textInputAction: textInputAction, + keyboardType: keyboardType, + decoration: InputDecoration( + hintText: hintText, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(36.0), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 30), + errorText: errorText, + ), + validator: validator, + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + controller: controller, + ), + ); + } +} diff --git a/lib/pangea/pages/sign_up/login_or_signup_view.dart b/lib/pangea/pages/sign_up/login_or_signup_view.dart new file mode 100644 index 000000000..c4bde2127 --- /dev/null +++ b/lib/pangea/pages/sign_up/login_or_signup_view.dart @@ -0,0 +1,25 @@ +import 'package:fluffychat/pangea/pages/sign_up/full_width_button.dart'; +import 'package:fluffychat/pangea/pages/sign_up/pangea_login_scaffold.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; + +class LoginOrSignupView extends StatelessWidget { + const LoginOrSignupView({super.key}); + + @override + Widget build(BuildContext context) { + return PangeaLoginScaffold( + children: [ + FullWidthButton( + title: Text(L10n.of(context).createAnAccount), + onPressed: () => context.go('/home/signup'), + ), + FullWidthButton( + title: Text(L10n.of(context).signIn), + onPressed: () => context.go('/home/login'), + ), + ], + ); + } +} diff --git a/lib/pangea/pages/sign_up/pangea_login_scaffold.dart b/lib/pangea/pages/sign_up/pangea_login_scaffold.dart new file mode 100644 index 000000000..da17656a0 --- /dev/null +++ b/lib/pangea/pages/sign_up/pangea_login_scaffold.dart @@ -0,0 +1,78 @@ +import 'dart:typed_data'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; + +class PangeaLoginScaffold extends StatelessWidget { + final String mainAssetPath; + final Uint8List? mainAssetBytes; + final List children; + final bool showAppName; + + const PangeaLoginScaffold({ + required this.children, + this.mainAssetPath = "assets/pangea/PangeaChat_Glow_Logo.png", + this.mainAssetBytes, + this.showAppName = true, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Scaffold( + appBar: AppBar(), + body: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 450, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 200, + height: 200, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.transparent, + ), + child: ClipOval( + child: mainAssetBytes != null + ? Image.memory( + mainAssetBytes!, + fit: BoxFit.cover, + ) + : Image.asset( + mainAssetPath, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(height: 16), + if (showAppName) + Text( + AppConfig.applicationName, + style: Theme.of(context).textTheme.displaySmall, + ), + const SizedBox(height: 16), + ...children, + ], + ), + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/pangea/pages/sign_up/pangea_login_view.dart b/lib/pangea/pages/sign_up/pangea_login_view.dart new file mode 100644 index 000000000..36a55cafd --- /dev/null +++ b/lib/pangea/pages/sign_up/pangea_login_view.dart @@ -0,0 +1,87 @@ +import 'package:fluffychat/pages/login/login.dart'; +import 'package:fluffychat/pangea/pages/connect/p_sso_button.dart'; +import 'package:fluffychat/pangea/pages/sign_up/full_width_button.dart'; +import 'package:fluffychat/pangea/pages/sign_up/pangea_login_scaffold.dart'; +import 'package:fluffychat/pangea/widgets/common/pangea_logo_svg.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class PangeaLoginView extends StatelessWidget { + final LoginController controller; + + const PangeaLoginView(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + return Form( + key: controller.formKey, + child: PangeaLoginScaffold( + children: [ + FullWidthTextField( + hintText: L10n.of(context).username, + autofocus: true, + textInputAction: TextInputAction.next, + validator: (value) { + if (value == null || value.isEmpty) { + return L10n.of(context).pleaseEnterYourUsername; + } + return null; + }, + controller: controller.usernameController, + errorText: controller.usernameError, + ), + FullWidthTextField( + hintText: L10n.of(context).password, + obscureText: true, + textInputAction: TextInputAction.done, + validator: (value) { + if (value == null || value.isEmpty) { + return L10n.of(context).pleaseEnterYourPassword; + } + return null; + }, + controller: controller.passwordController, + errorText: controller.passwordError, + ), + FullWidthButton( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PangeaLogoSvg( + width: 20, + forceColor: Theme.of(context).colorScheme.onPrimary, + ), + const SizedBox(width: 10), + Text(L10n.of(context).signIn), + ], + ), + onPressed: controller.enabledSignIn ? controller.login : null, + loading: controller.loading, + enabled: controller.enabledSignIn, + ), + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + const Expanded(child: Divider()), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text(L10n.of(context).or), + ), + const Expanded(child: Divider()), + ], + ), + ), + PangeaSsoButton( + provider: SSOProvider.google, + title: L10n.of(context).signInWithGoogle, + ), + PangeaSsoButton( + provider: SSOProvider.apple, + title: L10n.of(context).signInWithApple, + ), + ], + ), + ); + } +} diff --git a/lib/pangea/pages/sign_up/signup.dart b/lib/pangea/pages/sign_up/signup.dart index 9c9e9f693..d65ecd8af 100644 --- a/lib/pangea/pages/sign_up/signup.dart +++ b/lib/pangea/pages/sign_up/signup.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/pangea/pages/sign_up/signup_view.dart'; +import 'package:fluffychat/pangea/pages/sign_up/signup_with_email_view.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; @@ -9,22 +10,67 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix_api_lite/model/matrix_exception.dart'; class SignupPage extends StatefulWidget { - const SignupPage({super.key}); + final bool withEmail; + const SignupPage({ + this.withEmail = false, + super.key, + }); @override SignupPageController createState() => SignupPageController(); } class SignupPageController extends State { + final TextEditingController usernameController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); - final TextEditingController password2Controller = TextEditingController(); final TextEditingController emailController = TextEditingController(); + + String? usernameText; + String? passwordText; + String? emailText; + String? error; bool loading = false; bool showPassword = false; bool noEmailWarningConfirmed = false; bool displaySecondPasswordField = false; + @override + void initState() { + super.initState(); + + usernameController.addListener(() { + _setStateOnTextChange(usernameText, usernameController.text); + usernameText = usernameController.text; + }); + + passwordController.addListener(() { + _setStateOnTextChange(passwordText, passwordController.text); + passwordText = passwordController.text; + }); + + emailController.addListener(() { + _setStateOnTextChange(emailText, emailController.text); + emailText = emailController.text; + }); + } + + bool get enableSignUp => + !loading && + isTnCChecked && + emailController.text.isNotEmpty && + usernameController.text.isNotEmpty && + passwordController.text.isNotEmpty; + + void _setStateOnTextChange(String? oldText, String newText) { + if ((oldText == null || oldText.isEmpty) && (newText.isNotEmpty)) { + setState(() {}); + } + if ((oldText != null && oldText.isNotEmpty) && (newText.isEmpty)) { + setState(() {}); + } + } + static const int minPassLength = 8; void toggleShowPassword() => setState(() => showPassword = !showPassword); @@ -63,9 +109,13 @@ class SignupPageController extends State { } String? emailTextFieldValidator(String? value) { - if (value!.isEmpty && !noEmailWarningConfirmed) { - noEmailWarningConfirmed = true; - return L10n.of(context).noEmailWarning; + // #Pangea + if (value == null || value.isEmpty) { + // if (value!.isEmpty && !noEmailWarningConfirmed) { + // noEmailWarningConfirmed = true; + // return L10n.of(context).noEmailWarning; + return L10n.of(context).pleaseEnterEmail; + // Pangea# } if (value.isNotEmpty && !value.contains('@')) { return L10n.of(context).pleaseEnterValidEmail; @@ -73,7 +123,6 @@ class SignupPageController extends State { return null; } - // #Pangea bool isTnCChecked = false; String? signupError; void onTncChange(bool? value) { @@ -81,21 +130,21 @@ class SignupPageController extends State { signupError = null; setState(() {}); } - // #Pangea void signup([_]) async { setState(() { error = null; }); - if (!formKey.currentState!.validate()) return; - // #Pangea + final valid = formKey.currentState!.validate(); if (!isTnCChecked) { setState(() { - signupError = 'Please agree to the Terms and Conditions'; + signupError = L10n.of(context).pleaseAgreeToTOS; }); + } + if (!valid || !isTnCChecked) { return; } - // #Pangea + setState(() { loading = true; }); @@ -114,7 +163,7 @@ class SignupPageController extends State { ); } - final displayname = Matrix.of(context).loginUsername!; + final displayname = usernameController.text; final localPart = displayname.toLowerCase().replaceAll(' ', '_'); final registerRes = await client.uiaRequestBackground( @@ -126,30 +175,25 @@ class SignupPageController extends State { ), ); - //@brord is this right?? - //#Pangea GoogleAnalytics.login("pangea", registerRes.userId); - //Pangea# - // Set displayname if (displayname != localPart && client.userID != null) { await client.setDisplayName( client.userID!, displayname, ); } - } on MatrixException catch (e) { + } on MatrixException catch (e, s) { if (e.error != MatrixError.M_THREEPID_IN_USE) { - rethrow; + ErrorHandler.logError(e: e, s: s); } + error = e.errorMessage; } catch (e, s) { - //#Pangea const cancelledString = "Exception: Request has been canceled"; if (e.toString() != cancelledString) { ErrorHandler.logError(e: e, s: s); - error = (e).toLocalizedString(context); } - // Pangea# + error = (e).toLocalizedString(context); } finally { if (mounted) { setState(() => loading = false); @@ -158,5 +202,6 @@ class SignupPageController extends State { } @override - Widget build(BuildContext context) => SignupPageView(this); + Widget build(BuildContext context) => + widget.withEmail ? SignupWithEmailView(this) : SignupPageView(this); } diff --git a/lib/pangea/pages/sign_up/signup_view.dart b/lib/pangea/pages/sign_up/signup_view.dart index e4d90c77e..9f2eb8320 100644 --- a/lib/pangea/pages/sign_up/signup_view.dart +++ b/lib/pangea/pages/sign_up/signup_view.dart @@ -1,9 +1,12 @@ // Flutter imports: -import 'package:fluffychat/pangea/widgets/signup/tos_checkbox.dart'; -import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; +import 'package:fluffychat/pangea/pages/connect/p_sso_button.dart'; +import 'package:fluffychat/pangea/pages/sign_up/full_width_button.dart'; +import 'package:fluffychat/pangea/pages/sign_up/pangea_login_scaffold.dart'; +import 'package:fluffychat/pangea/widgets/common/pangea_logo_svg.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; import 'signup.dart'; @@ -13,153 +16,31 @@ class SignupPageView extends StatelessWidget { @override Widget build(BuildContext context) { - return LoginScaffold( - appBar: AppBar( - leading: controller.loading ? null : const BackButton(), - automaticallyImplyLeading: !controller.loading, - title: Text( - L10n.of(context).signUp, - // #Pangea - style: const TextStyle(color: Colors.white), - // #Pangea + return PangeaLoginScaffold( + children: [ + FullWidthButton( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PangeaLogoSvg( + width: 20, + forceColor: Theme.of(context).colorScheme.onPrimary, + ), + const SizedBox(width: 10), + Text(L10n.of(context).signUpWithEmail), + ], + ), + onPressed: () => context.go('/home/signup/email'), ), - ), - body: Form( - key: controller.formKey, - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: TextFormField( - readOnly: controller.loading, - autocorrect: false, - onChanged: controller.onPasswordType, - autofillHints: - controller.loading ? null : [AutofillHints.newPassword], - controller: controller.passwordController, - obscureText: !controller.showPassword, - validator: controller.password1TextFieldValidator, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.vpn_key_outlined), - suffixIcon: IconButton( - tooltip: L10n.of(context).showPassword, - icon: Icon( - controller.showPassword - ? Icons.visibility_off_outlined - : Icons.visibility_outlined, - color: Colors.black, - ), - onPressed: controller.toggleShowPassword, - ), - // #Pangea - // errorStyle: const TextStyle(color: Colors.orange), - errorStyle: TextStyle( - color: Theme.of(context).textTheme.bodyMedium?.color, - fontSize: 14, - ), - // Pangea# - hintText: L10n.of(context).chooseAStrongPassword, - // #Pangea - fillColor: Theme.of(context) - .colorScheme - .background - .withOpacity(0.75), - // #Pangea - ), - ), - ), - if (controller.displaySecondPasswordField) - Padding( - padding: const EdgeInsets.all(12.0), - child: TextFormField( - readOnly: controller.loading, - autocorrect: false, - autofillHints: - controller.loading ? null : [AutofillHints.newPassword], - controller: controller.password2Controller, - obscureText: !controller.showPassword, - validator: controller.password2TextFieldValidator, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.repeat_outlined), - hintText: L10n.of(context).repeatPassword, - // #Pangea - // errorStyle: const TextStyle(color: Colors.orange), - errorStyle: TextStyle( - color: Theme.of(context).textTheme.bodyMedium?.color, - fontSize: 14, - ), - fillColor: Theme.of(context) - .colorScheme - .background - .withOpacity(0.75), - // #Pangea - ), - ), - ), - Padding( - padding: const EdgeInsets.all(12.0), - child: TextFormField( - readOnly: controller.loading, - autocorrect: false, - controller: controller.emailController, - keyboardType: TextInputType.emailAddress, - autofillHints: - controller.loading ? null : [AutofillHints.username], - validator: controller.emailTextFieldValidator, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.mail_outlined), - hintText: L10n.of(context).enterAnEmailAddress, - errorText: controller.error, - errorMaxLines: 4, - // #Pangea - fillColor: Theme.of(context) - .colorScheme - .background - .withOpacity(0.75), - // errorStyle: TextStyle( - // color: controller.emailController.text.isEmpty - // ? Colors.orangeAccent - // : Colors.orange, - // ), - errorStyle: TextStyle( - color: Theme.of(context).textTheme.bodyMedium?.color, - fontSize: 14, - ), - // Pangea# - ), - ), - ), - // #Pangea - TosCheckbox(controller), - // #Pangea - Hero( - tag: 'loginButton', - child: Padding( - padding: const EdgeInsets.all(12), - // #Pangea - child: ElevatedButton( - onPressed: controller.loading ? () {} : controller.signup, - child: controller.loading - ? const LinearProgressIndicator() - : Text(L10n.of(context).signUp), - ), - // child: ElevatedButton.icon( - // icon: const Icon(Icons.person_add_outlined), - // style: ElevatedButton.styleFrom( - // foregroundColor: Theme.of(context).colorScheme.onPrimary, - // backgroundColor: Theme.of(context).colorScheme.primary, - // ), - // onPressed: controller.loading ? () {} : controller.signup, - // label: controller.loading - // ? const LinearProgressIndicator() - // : Text(L10n.of(context).signUp), - // ), - // #Pangea - ), - ), - ], + PangeaSsoButton( + provider: SSOProvider.google, + title: L10n.of(context).signUpWithGoogle, ), - ), + PangeaSsoButton( + provider: SSOProvider.apple, + title: L10n.of(context).signUpWithApple, + ), + ], ); } } diff --git a/lib/pangea/pages/sign_up/signup_with_email_view.dart b/lib/pangea/pages/sign_up/signup_with_email_view.dart new file mode 100644 index 000000000..6df13d599 --- /dev/null +++ b/lib/pangea/pages/sign_up/signup_with_email_view.dart @@ -0,0 +1,70 @@ +// Flutter imports: + +import 'package:fluffychat/pangea/pages/sign_up/full_width_button.dart'; +import 'package:fluffychat/pangea/pages/sign_up/pangea_login_scaffold.dart'; +import 'package:fluffychat/pangea/widgets/common/pangea_logo_svg.dart'; +import 'package:fluffychat/pangea/widgets/signup/tos_checkbox.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'signup.dart'; + +class SignupWithEmailView extends StatelessWidget { + final SignupPageController controller; + const SignupWithEmailView(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + return Form( + key: controller.formKey, + child: PangeaLoginScaffold( + children: [ + FullWidthTextField( + hintText: L10n.of(context).yourUsername, + autofocus: true, + textInputAction: TextInputAction.next, + validator: (text) { + if (text == null || text.isEmpty) { + return L10n.of(context).pleaseChooseAUsername; + } + return null; + }, + controller: controller.usernameController, + ), + FullWidthTextField( + hintText: L10n.of(context).yourEmail, + textInputAction: TextInputAction.next, + keyboardType: TextInputType.emailAddress, + validator: controller.emailTextFieldValidator, + controller: controller.emailController, + ), + FullWidthTextField( + hintText: L10n.of(context).password, + textInputAction: TextInputAction.done, + obscureText: true, + validator: controller.password1TextFieldValidator, + controller: controller.passwordController, + ), + TosCheckbox(controller), + FullWidthButton( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PangeaLogoSvg( + width: 20, + forceColor: Theme.of(context).colorScheme.onPrimary, + ), + const SizedBox(width: 10), + Text(L10n.of(context).signUp), + ], + ), + onPressed: controller.enableSignUp ? controller.signup : null, + error: controller.error, + loading: controller.loading, + enabled: controller.enableSignUp, + ), + ], + ), + ); + } +} diff --git a/lib/pangea/pages/sign_up/user_settings.dart b/lib/pangea/pages/sign_up/user_settings.dart new file mode 100644 index 000000000..76d7aa883 --- /dev/null +++ b/lib/pangea/pages/sign_up/user_settings.dart @@ -0,0 +1,169 @@ +import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/pages/sign_up/user_settings_view.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/utils/file_selector.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +class UserSettingsPage extends StatefulWidget { + const UserSettingsPage({super.key}); + + @override + UserSettingsState createState() => UserSettingsState(); +} + +class UserSettingsState extends State { + PangeaController get _pangeaController => MatrixState.pangeaController; + + LanguageModel? selectedTargetLanguage; + + String? selectedLanguageError; + String? profileCreationError; + + bool loading = false; + + Uint8List? avatar; + String? _selectedFilePath; + + List avatarPaths = const [ + "assets/pangea/Avatar_1.png", + "assets/pangea/Avatar_2.png", + "assets/pangea/Avatar_3.png", + "assets/pangea/Avatar_4.png", + "assets/pangea/Avatar_5.png", + ]; + String? selectedAvatarPath; + + LanguageModel? get _systemLanguage { + final systemLangCode = + _pangeaController.languageController.systemLanguage?.langCode; + return systemLangCode == null + ? null + : PangeaLanguage.byLangCode(systemLangCode); + } + + @override + void initState() { + super.initState(); + selectedTargetLanguage = _pangeaController.languageController.userL2; + selectedAvatarPath = avatarPaths.first; + } + + void setSelectedTargetLanguage(LanguageModel? language) { + setState(() { + selectedTargetLanguage = language; + selectedLanguageError = null; + }); + } + + void setSelectedAvatarPath(int index) { + if (index < 0 || index >= avatarPaths.length) return; + setState(() { + avatar = null; + selectedAvatarPath = avatarPaths[index]; + }); + } + + int get selectedAvatarIndex { + if (selectedAvatarPath == null) return -1; + return avatarPaths.indexOf(selectedAvatarPath!); + } + + void uploadAvatar() async { + final photo = await selectFiles( + context, + type: FileSelectorType.images, + allowMultiple: false, + ); + final selectedFile = photo.singleOrNull; + final bytes = await selectedFile?.readAsBytes(); + final path = selectedFile?.path; + + setState(() { + selectedAvatarPath = null; + avatar = bytes; + _selectedFilePath = path; + }); + } + + Future _setAvatar() async { + final client = Matrix.of(context).client; + try { + MatrixFile? file; + if (avatar != null && _selectedFilePath != null) { + file = MatrixFile( + bytes: avatar!, + name: _selectedFilePath!, + ); + } else if (selectedAvatarPath != null) { + final ByteData byteData = await rootBundle.load(selectedAvatarPath!); + final Uint8List bytes = byteData.buffer.asUint8List(); + file = MatrixFile( + bytes: bytes, + name: selectedAvatarPath!, + ); + } + if (file != null) await client.setAvatar(file); + } catch (err, s) { + ErrorHandler.logError(e: err, s: s); + } + } + + Future createUserInPangea() async { + setState(() => profileCreationError = null); + + if (selectedTargetLanguage == null) { + setState(() { + selectedLanguageError = L10n.of(context).pleaseSelectALanguage; + }); + return; + } + + setState(() => loading = true); + + try { + final updateFuture = [ + _setAvatar(), + _pangeaController.subscriptionController.reinitialize(), + _pangeaController.userController.updateProfile( + (profile) { + if (_systemLanguage != null) { + profile.userSettings.sourceLanguage = _systemLanguage!.langCode; + } + profile.userSettings.targetLanguage = + selectedTargetLanguage!.langCode; + profile.userSettings.createdAt = DateTime.now(); + return profile; + }, + waitForDataInSync: true, + ), + ]; + await Future.wait(updateFuture); + context.go('/rooms'); + } catch (err) { + if (err is MatrixException) { + profileCreationError = err.errorMessage; + } else { + profileCreationError = err.toLocalizedString(context); + } + if (mounted) setState(() {}); + } finally { + if (mounted) { + setState(() => loading = false); + } + } + } + + List get targetOptions => + _pangeaController.pLanguageStore.targetOptions; + + @override + Widget build(BuildContext context) => UserSettingsView(controller: this); +} diff --git a/lib/pangea/pages/sign_up/user_settings_view.dart b/lib/pangea/pages/sign_up/user_settings_view.dart new file mode 100644 index 000000000..a7eecb82b --- /dev/null +++ b/lib/pangea/pages/sign_up/user_settings_view.dart @@ -0,0 +1,148 @@ +import 'package:collection/collection.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/pages/sign_up/full_width_button.dart'; +import 'package:fluffychat/pangea/pages/sign_up/pangea_login_scaffold.dart'; +import 'package:fluffychat/pangea/pages/sign_up/user_settings.dart'; +import 'package:fluffychat/pangea/widgets/user_settings/p_language_dropdown.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class UserSettingsView extends StatelessWidget { + final UserSettingsState controller; + + const UserSettingsView({ + required this.controller, + super.key, + }); + + @override + Widget build(BuildContext context) { + final List avatarOptions = controller.avatarPaths + .mapIndexed((index, path) { + return Padding( + padding: const EdgeInsets.all(5), + child: AvatarOption( + onTap: () => controller.setSelectedAvatarPath(index), + path: path, + selected: controller.selectedAvatarIndex == index, + ), + ); + }) + .cast() + .toList(); + + avatarOptions.add( + Padding( + padding: const EdgeInsets.all(5), + child: InkWell( + onTap: controller.uploadAvatar, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + border: Border.all( + color: controller.avatar != null + ? AppConfig.activeToggleColor + : Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + child: Icon( + Icons.upload, + color: Theme.of(context).colorScheme.onPrimary, + size: 30, + ), + ), + ), + ), + ); + + return PangeaLoginScaffold( + showAppName: false, + mainAssetPath: controller.selectedAvatarPath ?? "", + mainAssetBytes: controller.avatar, + children: [ + Opacity( + opacity: 0.9, + child: Text( + L10n.of(context).chooseYourAvatar, + style: const TextStyle( + fontWeight: FontWeight.w100, + fontStyle: FontStyle.italic, + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: avatarOptions, + ), + Padding( + padding: const EdgeInsets.all(8), + child: PLanguageDropdown( + languages: controller.targetOptions, + onChange: controller.setSelectedTargetLanguage, + initialLanguage: controller.selectedTargetLanguage, + isL2List: true, + error: controller.selectedLanguageError, + ), + ), + FullWidthButton( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [Text(L10n.of(context).letsStart)], + ), + onPressed: controller.selectedTargetLanguage != null + ? controller.createUserInPangea + : null, + error: controller.profileCreationError, + loading: controller.loading, + enabled: controller.selectedTargetLanguage != null, + ), + ], + ); + } +} + +class AvatarOption extends StatelessWidget { + final VoidCallback onTap; + final String path; // Path or URL of the SVG file + final double size; // Diameter of the circle + final bool selected; + + const AvatarOption({ + super.key, + required this.onTap, + required this.path, + this.size = 50.0, + this.selected = false, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + border: Border.all( + color: selected + ? AppConfig.activeToggleColor + : Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + child: ClipOval( + child: Image.asset( + path, + fit: BoxFit.cover, // scale properly without warping + ), + ), + ), + ); + } +} diff --git a/lib/pangea/utils/sso_login_action.dart b/lib/pangea/utils/sso_login_action.dart new file mode 100644 index 000000000..bd030aecf --- /dev/null +++ b/lib/pangea/utils/sso_login_action.dart @@ -0,0 +1,65 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:matrix/matrix.dart'; +import 'package:universal_html/html.dart' as html; + +Future pangeaSSOLoginAction( + IdentityProvider provider, + Client client, + BuildContext context, +) async { + final bool isDefaultPlatform = + (PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS); + + final redirectUrl = kIsWeb + ? Uri.parse(html.window.location.href) + .resolveUri( + Uri(pathSegments: ['auth.html']), + ) + .toString() + : isDefaultPlatform + ? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login' + : 'http://localhost:3001//login'; + + final url = Uri.parse( + "${AppConfig.defaultHomeserver}/_matrix/client/v3/login/sso/redirect${provider.id == null ? '' : '/${provider.id}'}", + ).replace( + scheme: "https", + queryParameters: {'redirectUrl': redirectUrl}, + ); + + final urlScheme = isDefaultPlatform + ? Uri.parse(redirectUrl).scheme + : "http://localhost:3001"; + + String result; + try { + result = await FlutterWebAuth2.authenticate( + url: url.toString(), + callbackUrlScheme: urlScheme, + options: const FlutterWebAuth2Options(), + ); + } catch (err) { + if (err is PlatformException && err.code == 'CANCELED') { + debugPrint("user cancelled SSO login"); + return; + } + rethrow; + } + + final token = Uri.parse(result).queryParameters['loginToken']; + if (token?.isEmpty ?? false) return; + + final loginRes = await client.login( + LoginType.mLoginToken, + token: token, + initialDeviceDisplayName: PlatformInfos.clientName, + ); + GoogleAnalytics.login(provider.name!, loginRes.userId); +} diff --git a/lib/pangea/widgets/pressable_button.dart b/lib/pangea/widgets/pressable_button.dart index bb06fdce6..75b02be0b 100644 --- a/lib/pangea/widgets/pressable_button.dart +++ b/lib/pangea/widgets/pressable_button.dart @@ -39,16 +39,21 @@ class PressableButtonState extends State Completer? _animationCompleter; StreamSubscription? _triggerAnimationSubscription; + // seperate the widget's depressed state from the internal + // state to enable animations when this changes + bool _depressed = false; + @override void initState() { super.initState(); + _depressed = widget.depressed; _controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: this, ); _tweenAnimation = Tween(begin: 0, end: widget.buttonHeight).animate(_controller); - if (!widget.depressed) { + if (!_depressed) { _triggerAnimationSubscription = widget.triggerAnimation?.listen((_) { _animationCompleter = Completer(); _animateUp(); @@ -57,15 +62,30 @@ class PressableButtonState extends State } } + @override + void didUpdateWidget(PressableButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (_depressed && !widget.depressed) { + _controller.forward().then((_) { + _depressed = widget.depressed; + _controller.reverse(); + }); + } else if (!_depressed && widget.depressed) { + _controller.forward().then((_) { + _depressed = widget.depressed; + }); + } + } + void _onTapDown(TapDownDetails? details) { - if (widget.depressed) return; + if (_depressed) return; _animationCompleter = Completer(); if (!mounted) return; _animateUp(); } void _animateUp() { - if (widget.depressed || !mounted) return; + if (_depressed || !mounted) return; _controller.forward().then((_) { _animationCompleter?.complete(); _animationCompleter = null; @@ -73,8 +93,11 @@ class PressableButtonState extends State } Future _onTapUp(TapUpDetails? details) async { + if (_animationCompleter != null) { + await _animationCompleter!.future; + } widget.onPressed?.call(); - if (widget.depressed) return; + if (_depressed) return; await _animateDown(); } @@ -90,7 +113,7 @@ class PressableButtonState extends State } void _onTapCancel() { - if (widget.depressed) return; + if (_depressed) return; if (mounted) _controller.reverse(); } @@ -103,37 +126,46 @@ class PressableButtonState extends State @override Widget build(BuildContext context) { - return GestureDetector( - onTapDown: _onTapDown, - onTapUp: _onTapUp, - onTapCancel: _onTapCancel, - child: AnimatedBuilder( - animation: _tweenAnimation, - builder: (context, child) { - return Container( - decoration: BoxDecoration( - color: Color.alphaBlend( - Colors.black.withOpacity(0.25), - widget.color, + return NotificationListener( + onNotification: (notification) { + _onTapDown(null); + _onTapUp(null); + return true; // Stop the notification from bubbling further + }, + child: GestureDetector( + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + child: AnimatedBuilder( + animation: _tweenAnimation, + builder: (context, child) { + return Container( + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.black.withOpacity(0.25), + widget.color, + ), + borderRadius: widget.borderRadius, ), + padding: EdgeInsets.only( + bottom: !_depressed + ? widget.buttonHeight - _tweenAnimation.value + : 0, + ), + child: child, + ); + }, + child: Container( + decoration: BoxDecoration( + color: widget.color, borderRadius: widget.borderRadius, ), - padding: EdgeInsets.only( - bottom: !widget.depressed - ? widget.buttonHeight - _tweenAnimation.value - : 0, - ), - child: child, - ); - }, - child: Container( - decoration: BoxDecoration( - color: widget.color, - borderRadius: widget.borderRadius, + child: widget.child, ), - child: widget.child, ), ), ); } } + +class ButtonPressedNotification extends Notification {} diff --git a/lib/pangea/widgets/signup/tos_checkbox.dart b/lib/pangea/widgets/signup/tos_checkbox.dart index 3c6496ae3..595fefe21 100644 --- a/lib/pangea/widgets/signup/tos_checkbox.dart +++ b/lib/pangea/widgets/signup/tos_checkbox.dart @@ -1,65 +1,87 @@ // Flutter imports: import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/pages/sign_up/signup.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class TosCheckbox extends StatelessWidget { +class TosCheckbox extends StatefulWidget { final SignupPageController controller; - const TosCheckbox(this.controller, {super.key}); + + const TosCheckbox( + this.controller, { + super.key, + }); + @override + TosCheckboxState createState() => TosCheckboxState(); +} + +class TosCheckboxState extends State + with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CheckboxListTile( - controlAffinity: ListTileControlAffinity.leading, - contentPadding: const EdgeInsets.symmetric( - horizontal: 8.0, - ), - value: controller.isTnCChecked, - activeColor: Theme.of(context).colorScheme.primary, - onChanged: controller.onTncChange, - title: InkWell( - onTap: () => - UrlLauncher(context, AppConfig.termsOfServiceUrl).launchUrl(), - child: RichText( - maxLines: 2, - text: TextSpan( - text: L10n.of(context).iAgreeToThe, - children: [ - //PTODO - make sure this is actually a link - TextSpan( - text: L10n.of(context).termsAndConditions, - style: const TextStyle(color: Colors.blue), + return Padding( + padding: const EdgeInsets.all(8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InkWell( + onTap: () => UrlLauncher(context, AppConfig.termsOfServiceUrl) + .launchUrl(), + child: Padding( + padding: const EdgeInsets.only(left: 15), + child: RichText( + text: TextSpan( + text: L10n.of(context).iAgreeToThe, + children: [ + TextSpan( + text: L10n.of(context).termsAndConditions, + style: const TextStyle( + decoration: TextDecoration.underline, + ), + ), + ], + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), ), - TextSpan( - text: L10n.of(context).andCertifyIAmAtLeast13YearsOfAge, - ), - ], - style: const TextStyle(color: Colors.white), - ), + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: widget.controller.signupError == null + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.only(top: 4, left: 30), + child: Text( + widget.controller.signupError!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ), + ), + ], ), ), - ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12), - margin: const EdgeInsets.only(top: 5), - child: Text( - controller.signupError ?? '', - style: TextStyle( - color: Theme.of(context).textTheme.bodyMedium?.color, - fontSize: 14, - ), - ), + Checkbox( + value: widget.controller.isTnCChecked, + activeColor: Theme.of(context).colorScheme.primary, + onChanged: widget.controller.onTncChange, ), - ), - ], + ], + ), ); } } diff --git a/lib/pangea/widgets/user_settings/p_language_dropdown.dart b/lib/pangea/widgets/user_settings/p_language_dropdown.dart index 19c2e71a5..2e2af9ed4 100644 --- a/lib/pangea/widgets/user_settings/p_language_dropdown.dart +++ b/lib/pangea/widgets/user_settings/p_language_dropdown.dart @@ -1,17 +1,20 @@ // Flutter imports: +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/enum/l2_support_enum.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../widgets/flag.dart'; class PLanguageDropdown extends StatefulWidget { final List languages; - final LanguageModel initialLanguage; + final LanguageModel? initialLanguage; final Function(LanguageModel) onChange; final bool showMultilingual; final bool isL2List; + final String? error; const PLanguageDropdown({ super.key, @@ -20,6 +23,7 @@ class PLanguageDropdown extends StatefulWidget { required this.initialLanguage, this.showMultilingual = false, required this.isL2List, + this.error, }); @override @@ -55,68 +59,71 @@ class _PLanguageDropdownState extends State { sortedLanguages.sort((a, b) => sortLanguages(a, b)); - return Padding( - padding: const EdgeInsets.all(12), - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.secondary, - width: 0.5, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.secondary, + width: 1, + ), + borderRadius: const BorderRadius.all(Radius.circular(36)), ), - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - child: DropdownButton( - // Initial Value - hint: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 10), - child: LanguageFlag( - language: widget.initialLanguage, + padding: const EdgeInsets.symmetric(horizontal: 24), + child: DropdownButton( + hint: Row( + children: [ + const Icon(Icons.language_outlined), + const SizedBox(width: 10), + Text(L10n.of(context).iWantToLearn), + ], + ), + isExpanded: true, + icon: const Icon(Icons.keyboard_arrow_down), + underline: Container(), + items: [ + if (widget.showMultilingual) + DropdownMenuItem( + value: LanguageModel.multiLingual(context), + child: LanguageDropDownEntry( + languageModel: LanguageModel.multiLingual(context), + isL2List: widget.isL2List, + ), ), - ), - const SizedBox(width: 10), - Text( - widget.initialLanguage.getDisplayName(context) ?? "", - style: const TextStyle().copyWith( - color: Theme.of(context).textTheme.bodyLarge!.color, - fontSize: 14, + ...sortedLanguages.map( + (languageModel) => DropdownMenuItem( + value: languageModel, + child: LanguageDropDownEntry( + languageModel: languageModel, + isL2List: widget.isL2List, + ), ), - overflow: TextOverflow.clip, - textAlign: TextAlign.center, ), ], + onChanged: (value) => widget.onChange(value!), + value: widget.initialLanguage, ), - - isExpanded: true, - // Down Arrow Icon - icon: const Icon(Icons.keyboard_arrow_down), - underline: Container(), - // Array list of items - items: [ - if (widget.showMultilingual) - DropdownMenuItem( - value: LanguageModel.multiLingual(context), - child: LanguageDropDownEntry( - languageModel: LanguageModel.multiLingual(context), - isL2List: widget.isL2List, - ), - ), - ...sortedLanguages.map( - (languageModel) => DropdownMenuItem( - value: languageModel, - child: LanguageDropDownEntry( - languageModel: languageModel, - isL2List: widget.isL2List, - ), - ), - ), - ], - onChanged: (value) => widget.onChange(value!), ), - ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: widget.error == null + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30, + vertical: 5, + ), + child: Text( + widget.error!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ), + ), + ], ); } } diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index cb04e367d..6d5032aff 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -82,8 +82,17 @@ class MatrixState extends State with WidgetsBindingObserver { widget.clients.add(getLoginClient()); } if (_activeClient < 0 || _activeClient >= widget.clients.length) { + // #Pangea + currentBundle!.first!.homeserver = + Uri.parse("https://${AppConfig.defaultHomeserver}"); + // Pangea# return currentBundle!.first!; } + + // #Pangea + widget.clients[_activeClient].homeserver = + Uri.parse("https://${AppConfig.defaultHomeserver}"); + // Pangea# return widget.clients[_activeClient]; } @@ -175,6 +184,9 @@ class MatrixState extends State with WidgetsBindingObserver { _loginClientCandidate = null; FluffyChatApp.router.go('/rooms'); }); + // #Pangea + candidate.homeserver = Uri.parse("https://${AppConfig.defaultHomeserver}"); + // Pangea# return candidate; } @@ -338,10 +350,10 @@ class MatrixState extends State with WidgetsBindingObserver { } String routeDestination; if (state == LoginState.loggedIn) { - routeDestination = await pangeaController - .userController.isUserDataAvailableAndDateOfBirthSet - ? '/rooms' - : "/user_age"; + routeDestination = + await pangeaController.userController.isUserDataAvailableAndL2Set + ? '/rooms' + : "/user_age"; } else { routeDestination = '/home'; }