mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 09:52:12 +00:00
343 lines
12 KiB
Dart
343 lines
12 KiB
Dart
// ignore_for_file: avoid_dynamic_calls
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:twonly/globals.dart';
|
|
import 'package:twonly/locator.dart';
|
|
import 'package:twonly/src/constants/routes.keys.dart';
|
|
import 'package:twonly/src/model/json/userdata.model.dart';
|
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
|
import 'package:twonly/src/services/signal/identity.signal.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/themes/light.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/components/onboarding_wrapper.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();
|
|
final TextEditingController inviteCodeController = TextEditingController();
|
|
|
|
bool _registrationDisabled = false;
|
|
bool _isTryingToRegister = false;
|
|
bool _isValidUserName = false;
|
|
bool _showUserNameError = false;
|
|
bool _showProofOfWorkError = false;
|
|
|
|
late Future<int>? proofOfWork;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
proofOfWork = widget.proofOfWork.$1;
|
|
_registrationDisabled = widget.proofOfWork.$2;
|
|
}
|
|
|
|
Future<void> createNewUser() async {
|
|
if (!_isValidUserName) {
|
|
setState(() {
|
|
_showUserNameError = true;
|
|
});
|
|
return;
|
|
}
|
|
final username = usernameController.text;
|
|
final inviteCode = inviteCodeController.text;
|
|
|
|
setState(() {
|
|
_isTryingToRegister = true;
|
|
_showUserNameError = false;
|
|
_showProofOfWorkError = false;
|
|
});
|
|
|
|
late int proof;
|
|
|
|
if (proofOfWork != null) {
|
|
proof = await proofOfWork!;
|
|
} else {
|
|
final (pow, registrationDisabled) = await apiService.getProofOfWork();
|
|
if (pow == null) {
|
|
_registrationDisabled = registrationDisabled;
|
|
if (mounted) {
|
|
showNetworkIssue(context);
|
|
}
|
|
return;
|
|
// Starting with the proof of work.
|
|
}
|
|
proof = await calculatePoW(pow.prefix, pow.difficulty.toInt());
|
|
}
|
|
|
|
Log.info('The result of the POW is $proof');
|
|
|
|
await createIfNotExistsSignalIdentity();
|
|
|
|
var userId = 0;
|
|
|
|
final res = await apiService.register(username, inviteCode, proof);
|
|
if (res.isSuccess) {
|
|
Log.info('Got user_id ${res.value} from server');
|
|
userId = res.value.userid.toInt() as int;
|
|
} else {
|
|
proofOfWork = null;
|
|
if (res.error == ErrorCode.RegistrationDisabled) {
|
|
_registrationDisabled = true;
|
|
return;
|
|
}
|
|
if (res.error == ErrorCode.UserIdAlreadyTaken) {
|
|
Log.error('User ID already token. Tying again.');
|
|
await deleteLocalUserData();
|
|
return createNewUser();
|
|
}
|
|
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 as ErrorCode),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isTryingToRegister = false;
|
|
});
|
|
|
|
final userData = UserData(
|
|
userId: userId,
|
|
username: username,
|
|
displayName: username,
|
|
subscriptionPlan: 'Free',
|
|
currentSetupPage: SetupPages.profile.name,
|
|
)..appVersion = AppState.latestAppVersionId;
|
|
|
|
await UserService.save(userData);
|
|
|
|
await apiService.authenticate();
|
|
widget.callbackOnSuccess();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = isDarkMode(context);
|
|
final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white;
|
|
final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100];
|
|
final sloganColor = isDark ? Colors.white.withValues(alpha: 0.9) : Colors.grey[800];
|
|
final secondaryButtonColor = isDark ? Colors.grey[400] : Colors.grey[600];
|
|
|
|
return OnboardingWrapper(
|
|
children: [
|
|
const SizedBox(height: 30),
|
|
Center(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(10),
|
|
child: const LinkLogoAnimation(),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Text(
|
|
context.lang.registerSlogan,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.white.withValues(alpha: 0.9),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: cardColor,
|
|
borderRadius: BorderRadius.circular(32),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: isDark ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.1),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 10),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (_registrationDisabled) ...[
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
context.lang.registrationClosed,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.red,
|
|
),
|
|
),
|
|
const SizedBox(height: 48),
|
|
] else ...[
|
|
Text(
|
|
context.lang.registerUsernameSlogan,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: sloganColor,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
TextField(
|
|
controller: usernameController,
|
|
onChanged: (value) {
|
|
usernameController.text = value.toLowerCase();
|
|
usernameController.selection = TextSelection.fromPosition(
|
|
TextPosition(
|
|
offset: usernameController.text.length,
|
|
),
|
|
);
|
|
setState(() {
|
|
_isValidUserName = usernameController.text.length >= 3;
|
|
});
|
|
},
|
|
inputFormatters: [
|
|
LengthLimitingTextInputFormatter(12),
|
|
FilteringTextInputFormatter.allow(
|
|
RegExp('[a-z0-9A-Z._]'),
|
|
),
|
|
],
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w500,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: context.lang.registerUsernameDecoration,
|
|
hintStyle: TextStyle(
|
|
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
|
),
|
|
filled: true,
|
|
fillColor: inputColor,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
prefixIcon: Icon(
|
|
Icons.alternate_email,
|
|
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
|
),
|
|
),
|
|
),
|
|
if (_showUserNameError && usernameController.text.length < 3) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
context.lang.registerUsernameLimits,
|
|
style: const TextStyle(
|
|
color: Colors.red,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
if (_showProofOfWorkError) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
context.lang.registerProofOfWorkFailed,
|
|
style: const TextStyle(
|
|
color: Colors.red,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
const SizedBox(height: 24),
|
|
FilledButton(
|
|
onPressed: _isTryingToRegister ? null : createNewUser,
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: primaryColor,
|
|
foregroundColor: Colors.white,
|
|
minimumSize: const Size.fromHeight(60),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(18),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
child: _isTryingToRegister
|
|
? const SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 3,
|
|
),
|
|
)
|
|
: Text(
|
|
context.lang.registerSubmitButton,
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
TextButton(
|
|
onPressed: () => context.push(
|
|
Routes.settingsBackupRecovery,
|
|
),
|
|
style: TextButton.styleFrom(
|
|
minimumSize: const Size.fromHeight(50),
|
|
foregroundColor: secondaryButtonColor,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(18),
|
|
),
|
|
),
|
|
child: Text(
|
|
context.lang.twonlySafeRecoverBtn,
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Spacer(),
|
|
const SizedBox(height: 40),
|
|
],
|
|
);
|
|
}
|
|
}
|