mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 08:54:08 +00:00
347 lines
12 KiB
Dart
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),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|