initial design updates to signup/login process (#1268)

* initial design updates to signup/login process

* feat: added signup/login assets, better button styling and animations

* fix: signup / login updated based on mobile testing
This commit is contained in:
ggurdin 2024-12-18 14:53:25 -05:00 committed by GitHub
parent d6d6875882
commit 16bdce9bd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1520 additions and 991 deletions

View file

@ -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"
}

BIN
assets/pangea/Avatar_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
assets/pangea/Avatar_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
assets/pangea/Avatar_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
assets/pangea/Avatar_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

BIN
assets/pangea/Avatar_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

View file

@ -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,
),

View file

@ -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<HomeserverPicker> {
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<HomeserverPicker> {
checkHomeserverAction();
}
// #Pangea
Map<String, dynamic>? _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<void> 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<HomeserverPicker> {
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<HomeserverPicker> {
: '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<HomeserverPicker> {
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<HomeserverPicker> {
}
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<HomeserverPicker> {
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<HomeserverPicker> {
}
}
// #Pangea
List<IdentityProvider>? 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:

View file

@ -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<MoreLoginActions>(
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<MoreLoginActions>(
// 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),
),
],
),
),
],
),
),
),
);

View file

@ -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<Login> {
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<FormState> formKey = GlobalKey<FormState>();
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<Login> {
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<Login> {
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<Login> {
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<Login> {
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 {

View file

@ -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();

View file

@ -84,7 +84,7 @@ class UserController extends BaseController {
}
/// Creates a new profile for the user with the given date of birth.
Future<void> createProfile({required DateTime dob}) async {
Future<void> 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<bool> get isUserDataAvailableAndDateOfBirthSet async {
/// Checks if user data is available and the user's l2 is set.
Future<bool> 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;

View file

@ -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");

View file

@ -34,7 +34,9 @@ class UserSettings {
});
factory UserSettings.fromJson(Map<String, dynamic> 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,

View file

@ -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<PangeaSsoButton> {
bool _loading = false;
String? _error;
Future<void> _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,
);
}
}

View file

@ -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<PUserAge> {
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<void> 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}",
),
),
);
}
}

View file

@ -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<int>(
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<int>(
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),
),
),
),
],
),
);
}
}

View file

@ -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<FullWidthButton> {
@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,
),
);
}
}

View file

@ -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'),
),
],
);
}
}

View file

@ -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<Widget> 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,
],
),
),
),
),
);
},
),
),
);
}
}

View file

@ -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,
),
],
),
);
}
}

View file

@ -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<SignupPage> {
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<SignupPage> {
}
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<SignupPage> {
return null;
}
// #Pangea
bool isTnCChecked = false;
String? signupError;
void onTncChange(bool? value) {
@ -81,21 +130,21 @@ class SignupPageController extends State<SignupPage> {
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<SignupPage> {
);
}
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<SignupPage> {
),
);
//@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<SignupPage> {
}
@override
Widget build(BuildContext context) => SignupPageView(this);
Widget build(BuildContext context) =>
widget.withEmail ? SignupWithEmailView(this) : SignupPageView(this);
}

View file

@ -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,
),
],
);
}
}

View file

@ -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,
),
],
),
);
}
}

View file

@ -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<UserSettingsPage> {
PangeaController get _pangeaController => MatrixState.pangeaController;
LanguageModel? selectedTargetLanguage;
String? selectedLanguageError;
String? profileCreationError;
bool loading = false;
Uint8List? avatar;
String? _selectedFilePath;
List<String> 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<void> _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<void> 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<LanguageModel> get targetOptions =>
_pangeaController.pLanguageStore.targetOptions;
@override
Widget build(BuildContext context) => UserSettingsView(controller: this);
}

View file

@ -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<Widget> 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<Widget>()
.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
),
),
),
);
}
}

View file

@ -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<void> 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);
}

View file

@ -39,16 +39,21 @@ class PressableButtonState extends State<PressableButton>
Completer<void>? _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<double>(begin: 0, end: widget.buttonHeight).animate(_controller);
if (!widget.depressed) {
if (!_depressed) {
_triggerAnimationSubscription = widget.triggerAnimation?.listen((_) {
_animationCompleter = Completer<void>();
_animateUp();
@ -57,15 +62,30 @@ class PressableButtonState extends State<PressableButton>
}
}
@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<void>();
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<PressableButton>
}
Future<void> _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<PressableButton>
}
void _onTapCancel() {
if (widget.depressed) return;
if (_depressed) return;
if (mounted) _controller.reverse();
}
@ -103,37 +126,46 @@ class PressableButtonState extends State<PressableButton>
@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<ButtonPressedNotification>(
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 {}

View file

@ -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<TosCheckbox>
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,
),
),
],
],
),
);
}
}

View file

@ -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<LanguageModel> 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<PLanguageDropdown> {
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<LanguageModel>(
// 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<LanguageModel>(
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,
),
),
),
),
],
);
}
}

View file

@ -82,8 +82,17 @@ class MatrixState extends State<Matrix> 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<Matrix> 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<Matrix> 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';
}