From a4e27e57a3df2f175b0896390c692c109a704d7e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 30 Apr 2025 00:22:12 +0800 Subject: [PATCH] :lipstick: Optimized account creation --- assets/i18n/en-US.json | 7 +- lib/pods/network.dart | 25 ++++--- lib/screens/auth/create_account.dart | 107 +++++++++++++++++++-------- lib/widgets/alert.dart | 25 ++++++- pubspec.lock | 12 ++- pubspec.yaml | 1 + 6 files changed, 134 insertions(+), 43 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 546ca88..104d81c 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -27,6 +27,7 @@ "logout": "Logout", "updateYourProfile": "Edit Profile", "accountBasicInfo": "Basic Info", + "accountProfile": "Your Profile", "saveChanges": "Save Changes", "publishers": "Publishers", "managedPublisher": "Managed Publishers", @@ -41,5 +42,9 @@ "somethingWentWrong": "Something went wrong...", "deletePost": "Delete Post", "deletePostHint": "Are you sure to delete this post?", - "copyLink": "Copy Link" + "copyLink": "Copy Link", + "postCreateAccountTitle": "Thanks for joining!", + "postCreateAccountNext": "What's next?", + "postCreateAccountNext1": "Go to your email inbox and receive the account activation email.", + "postCreateAccountNext2": "Log in to your account and start exploring the Solar Network!" } diff --git a/lib/pods/network.dart b/lib/pods/network.dart index b86a573..a3b4fdc 100644 --- a/lib/pods/network.dart +++ b/lib/pods/network.dart @@ -67,17 +67,22 @@ final apiClientProvider = Provider((ref) { RequestOptions options, RequestInterceptorHandler handler, ) async { - final atk = await getFreshAtk( - ref.watch(tokenPairProvider), - ref.watch(serverUrlProvider), - onRefreshed: (atk, rtk) { - setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk); - ref.invalidate(tokenPairProvider); - }, - ); - if (atk != null) { - options.headers['Authorization'] = 'Bearer $atk'; + try { + final atk = await getFreshAtk( + ref.watch(tokenPairProvider), + ref.watch(serverUrlProvider), + onRefreshed: (atk, rtk) { + setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk); + ref.invalidate(tokenPairProvider); + }, + ); + if (atk != null) { + options.headers['Authorization'] = 'Bearer $atk'; + } + } catch (err) { + // ignore } + final userAgent = ref.watch(userAgentProvider); if (userAgent.value != null) { options.headers['User-Agent'] = userAgent.value; diff --git a/lib/screens/auth/create_account.dart b/lib/screens/auth/create_account.dart index 32d5635..ddcea58 100644 --- a/lib/screens/auth/create_account.dart +++ b/lib/screens/auth/create_account.dart @@ -10,6 +10,7 @@ import 'package:island/route.gr.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:lucide_icons/lucide_icons.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -23,21 +24,21 @@ class CreateAccountScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final formKey = useMemoized(GlobalKey.new, const []); - final email = useState(''); - final username = useState(''); - final nickname = useState(''); - final password = useState(''); + final emailController = useTextEditingController(); + final usernameController = useTextEditingController(); + final nicknameController = useTextEditingController(); + final passwordController = useTextEditingController(); + + void showPostCreateModal() { + showCupertinoModalBottomSheet( + context: context, + builder: (context) => _PostCreateModal(), + ); + } void performAction() async { if (!formKey.currentState!.validate()) return; - if (email.value.isEmpty || - username.value.isEmpty || - nickname.value.isEmpty || - password.value.isEmpty) { - return; - } - final captchaTk = await Navigator.of( context, ).push(MaterialPageRoute(builder: (context) => CaptchaScreen())); @@ -48,19 +49,19 @@ class CreateAccountScreen extends HookConsumerWidget { try { final client = ref.watch(apiClientProvider); await client.post( - '/users', + '/accounts', data: { - 'name': username.value, - 'nick': nickname.value, - 'email': email.value, - 'password': password.value, + 'name': usernameController.text, + 'nick': nicknameController.text, + 'email': emailController.text, + 'password': passwordController.text, 'language': EasyLocalization.of(context)!.currentLocale.toString(), 'captcha_token': captchaTk, }, ); if (!context.mounted) return; - context.router.replace(CreateAccountRoute()); + showPostCreateModal(); } catch (err) { showErrorAlert(err); } @@ -99,7 +100,7 @@ class CreateAccountScreen extends HookConsumerWidget { child: Column( children: [ TextFormField( - initialValue: username.value, + controller: usernameController, validator: (value) { if (value == null || value.isEmpty) { return 'fieldCannotBeEmpty'.tr(); @@ -108,12 +109,12 @@ class CreateAccountScreen extends HookConsumerWidget { }, autocorrect: false, enableSuggestions: false, - onSaved: (val) => username.value = val ?? '', autofillHints: const [AutofillHints.username], decoration: InputDecoration( isDense: true, border: const UnderlineInputBorder(), labelText: 'username'.tr(), + helperText: 'usernameCannotChangeHint'.tr(), ), onTapOutside: (_) => @@ -122,7 +123,7 @@ class CreateAccountScreen extends HookConsumerWidget { ), const Gap(12), TextFormField( - initialValue: nickname.value, + controller: nicknameController, validator: (value) { if (value == null || value.isEmpty) { return 'fieldCannotBeEmpty'.tr(); @@ -130,8 +131,6 @@ class CreateAccountScreen extends HookConsumerWidget { return null; }, autocorrect: false, - enableSuggestions: false, - onSaved: (val) => nickname.value = val ?? '', autofillHints: const [AutofillHints.nickname], decoration: InputDecoration( isDense: true, @@ -145,7 +144,7 @@ class CreateAccountScreen extends HookConsumerWidget { ), const Gap(12), TextFormField( - initialValue: email.value, + controller: emailController, validator: (value) { if (value == null || value.isEmpty) { return 'fieldCannotBeEmpty'.tr(); @@ -157,7 +156,6 @@ class CreateAccountScreen extends HookConsumerWidget { }, autocorrect: false, enableSuggestions: false, - onSaved: (val) => email.value = val ?? '', autofillHints: const [AutofillHints.email], decoration: InputDecoration( isDense: true, @@ -171,7 +169,7 @@ class CreateAccountScreen extends HookConsumerWidget { ), const Gap(12), TextFormField( - initialValue: password.value, + controller: passwordController, validator: (value) { if (value == null || value.isEmpty) { return 'fieldCannotBeEmpty'.tr(); @@ -181,7 +179,6 @@ class CreateAccountScreen extends HookConsumerWidget { obscureText: true, autocorrect: false, enableSuggestions: false, - onSaved: (val) => password.value = val ?? '', autofillHints: const [AutofillHints.password], decoration: InputDecoration( isDense: true, @@ -245,9 +242,7 @@ class CreateAccountScreen extends HookConsumerWidget { alignment: Alignment.centerRight, child: TextButton( onPressed: () { - if (formKey.currentState?.validate() ?? false) { - performAction(); - } + performAction(); }, child: Row( mainAxisSize: MainAxisSize.min, @@ -266,3 +261,57 @@ class CreateAccountScreen extends HookConsumerWidget { ); } } + +class _PostCreateModal extends HookConsumerWidget { + const _PostCreateModal(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Center( + child: Material( + color: Colors.transparent, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 280), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('🎉').fontSize(32), + Text( + 'postCreateAccountTitle'.tr(), + textAlign: TextAlign.center, + ).fontSize(17), + const Gap(18), + Text('postCreateAccountNext').tr().fontSize(19).bold(), + const Gap(4), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 6, + children: [ + Text('\u2022'), + Expanded(child: Text('postCreateAccountNext1').tr()), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 6, + children: [ + Text('\u2022'), + Expanded(child: Text('postCreateAccountNext2').tr()), + ], + ), + const Gap(6), + TextButton( + onPressed: () { + Navigator.pop(context); + context.router.replace(LoginRoute()); + }, + child: Text('login'.tr()), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/alert.dart b/lib/widgets/alert.dart index bfda3ea..9495c4c 100644 --- a/lib/widgets/alert.dart +++ b/lib/widgets/alert.dart @@ -1,10 +1,33 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_platform_alert/flutter_platform_alert.dart'; +String _parseRemoteError(DioException err) { + if (err.response?.data is String) return err.response?.data; + if (err.response?.data?['errors'] != null) { + final errors = err.response?.data['errors'] as Map; + return errors.values + .map( + (ele) => + (ele as List).map((ele) => ele.toString()).join('\n'), + ) + .join('\n'); + } + return jsonEncode(err.response?.data); +} + void showErrorAlert(dynamic err) async { + final text = switch (err) { + String _ => err, + DioException _ => _parseRemoteError(err), + Exception _ => err.toString(), + _ => err.toString(), + }; FlutterPlatformAlert.showAlert( windowTitle: 'somethingWentWrong'.tr(), - text: err.toString(), + text: text, alertStyle: AlertButtonStyle.ok, iconStyle: IconStyle.error, ); diff --git a/pubspec.lock b/pubspec.lock index 99e069c..8b153a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1045,6 +1045,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + modal_bottom_sheet: + dependency: "direct main" + description: + name: modal_bottom_sheet + sha256: eac66ef8cb0461bf069a38c5eb0fa728cee525a531a8304bd3f7b2185407c67e + url: "https://pub.dev" + source: hosted + version: "3.0.0" nested: dependency: transitive description: @@ -1735,10 +1743,10 @@ packages: dependency: transitive description: name: wakelock_plus - sha256: b6962cd9fc15e4843b573ba7b53bc46dd8a787594cf9ed5c5182581924656a58 + sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678 url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" wakelock_plus_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ab69567..fa7d90e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,7 @@ dependencies: image_picker_platform_interface: ^2.10.1 image_picker_android: ^0.8.12+23 super_context_menu: ^0.9.0-dev.6 + modal_bottom_sheet: ^3.0.0 dev_dependencies: flutter_test: