twonly-app/lib/src/visual/views/onboarding/register.view.dart
2026-08-29 15:43:32 +02:00

347 lines
12 KiB
Dart

// 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<int>?, bool) proofOfWork;
@override
State<RegisterView> createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final TextEditingController usernameController = TextEditingController();
bool _registrationDisabled = false;
bool _isTryingToRegister = false;
bool _isValidUserName = false;
bool _showProofOfWorkError = false;
String? _usernameErrorText;
late Future<int>? proofOfWork;
@override
void initState() {
super.initState();
proofOfWork = widget.proofOfWork.$1;
_registrationDisabled = widget.proofOfWork.$2;
}
Future<void> 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),
],
),
),
),
);
},
),
),
),
);
}
}