// ignore_for_file: avoid_dynamic_calls import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/core/bridge/user_config.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/elements/my_input.element.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/onboarding/components/link_logo_animation.dart'; import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; class RegisterView extends StatefulWidget { const RegisterView({ required this.callbackOnSuccess, required this.proofOfWork, super.key, }); final Function callbackOnSuccess; final (Future?, bool) proofOfWork; @override State createState() => _RegisterViewState(); } class _RegisterViewState extends State { final TextEditingController usernameController = TextEditingController(); bool _registrationDisabled = false; bool _isTryingToRegister = false; bool _isValidUserName = false; bool _showProofOfWorkError = false; String? _usernameErrorText; late Future? proofOfWork; @override void initState() { super.initState(); proofOfWork = widget.proofOfWork.$1; _registrationDisabled = widget.proofOfWork.$2; } Future createNewUser() async { if (!_isValidUserName) { setState(() { _usernameErrorText = context.lang.registerUsernameLimits; }); return; } final username = usernameController.text; setState(() { _isTryingToRegister = true; _usernameErrorText = null; _showProofOfWorkError = false; }); try { late int proof; if (proofOfWork != null) { proof = await proofOfWork!; } else { final proofResult = await rustApiResult(RustApi.getProofOfWork()); final pow = proofResult.value; final registrationDisabled = proofResult.error == ErrorCode.RegistrationDisabled; if (pow == null) { setState(() { _registrationDisabled = registrationDisabled; _isTryingToRegister = false; }); if (mounted) { showNetworkIssue(context); } return; } proof = await calculatePoW(pow.prefix, pow.difficulty); } Log.info('The result of the POW is $proof'); var userId = 0; final res = await rustApiResult( RustApi.register( username: username, proofOfWork: proof, langCode: ui.PlatformDispatcher.instance.locale.languageCode, isIos: Platform.isIOS, ), ); if (res.isSuccess) { Log.info('Got user_id ${res.value} from server'); userId = res.value!; } else { proofOfWork = null; if (res.error == ErrorCode.RegistrationDisabled) { setState(() { _registrationDisabled = true; _isTryingToRegister = false; }); return; } if (res.error == ErrorCode.UserIdAlreadyTaken) { Log.error('User ID already token. Tying again.'); await deleteLocalUserData(); return await createNewUser(); } if (res.error == ErrorCode.UsernameAlreadyTaken || res.error == ErrorCode.UsernameNotValid) { setState(() { _usernameErrorText = errorCodeToText( context, res.error!, ); _isTryingToRegister = false; }); return; } if (res.error == ErrorCode.InvalidProofOfWork) { await deleteLocalUserData(); setState(() { _showProofOfWorkError = true; _isTryingToRegister = false; }); return; } if (mounted) { setState(() { _isTryingToRegister = false; }); await showAlertDialog( context, 'Oh no!', errorCodeToText(context, res.error!), ); } return; } setState(() { _isTryingToRegister = false; }); final userData = await UserConfigApi.create( userId: userId, username: username, displayName: username, currentSetupPage: SetupPages.profile.name, appVersion: AppState.latestAppVersionId, ); await UserService.save(userData); unawaited(FcmNotificationService.initAfterUserLoaded()); await RustApi.reloadConfiguration(); widget.callbackOnSuccess(); } catch (e, stack) { Log.error('Error creating new user', error: e, stackTrace: stack); await deleteLocalUserData(); if (mounted) { setState(() { _isTryingToRegister = false; }); await showAlertDialog( context, 'Error', e.toString(), ); } } } @override Widget build(BuildContext context) { final isDark = isDarkMode(context); return GestureDetector( onTap: () => FocusManager.instance.primaryFocus?.unfocus(), behavior: HitTestBehavior.opaque, child: Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: SafeArea( child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24), child: ConstrainedBox( constraints: BoxConstraints( minHeight: constraints.maxHeight, ), child: IntrinsicHeight( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 30), Center( child: Container( padding: const EdgeInsets.all(10), child: LinkLogoAnimation( color: isDark ? Colors.white : Colors.black, ), ), ), const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Text( context.lang.registerSlogan, textAlign: TextAlign.center, style: TextStyle( fontSize: 16, color: Theme.of(context).textTheme.bodyMedium?.color ?.withValues(alpha: 0.7) ?? (isDark ? Colors.white.withValues(alpha: 0.7) : Colors.black.withValues(alpha: 0.7)), fontWeight: FontWeight.w500, ), ), ), const SizedBox(height: 40), if (_registrationDisabled) ...[ Text( context.lang.registrationClosed, textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, color: Colors.redAccent, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 40), ] else ...[ Text( context.lang.registerUsernameSlogan, textAlign: TextAlign.center, style: TextStyle( fontSize: 22, color: isDark ? Colors.white : Colors.black87, fontWeight: FontWeight.bold, letterSpacing: -0.5, ), ), const SizedBox(height: 24), MyInput( controller: usernameController, errorText: _usernameErrorText, onChanged: (value) { usernameController.text = value.toLowerCase(); usernameController.selection = TextSelection.fromPosition( TextPosition( offset: usernameController.text.length, ), ); setState(() { _isValidUserName = usernameController.text.length >= 3; _usernameErrorText = null; }); }, inputFormatters: [ LengthLimitingTextInputFormatter(12), FilteringTextInputFormatter.allow( RegExp('[a-z0-9A-Z._]'), ), ], hintText: context.lang.registerUsernameDecoration, prefixIcon: const Icon(Icons.alternate_email), ), if (_showProofOfWorkError) ...[ const SizedBox(height: 10), Text( context.lang.registerProofOfWorkFailed, style: const TextStyle( color: Colors.redAccent, fontSize: 14, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), ], const SizedBox(height: 32), MyButton( onPressed: _isTryingToRegister ? null : createNewUser, child: _isTryingToRegister ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator.adaptive( valueColor: AlwaysStoppedAnimation( Colors.white, ), strokeWidth: 3, ), ) : Text( context.lang.registerSubmitButton, ), ), const SizedBox(height: 20), ], MyButton( onPressed: () => context.push( Routes.settingsBackupRecovery, ), variant: MyButtonVariant.secondary, child: Text( context.lang.twonlySafeRecoverBtn, ), ), const Spacer(), const SizedBox(height: 40), ], ), ), ), ); }, ), ), ), ); } }