feat(apps): integrate auth cubits with register and login screens
This commit is contained in:
@@ -35,7 +35,7 @@ class _LoginEmailScreenState extends State<LoginEmailScreen> {
|
||||
setState(() {
|
||||
_showWarning = false;
|
||||
});
|
||||
context.push('/login/password');
|
||||
context.push('/login/password', extra: _emailController.text);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,25 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:formz/formz.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/warning_banner.dart';
|
||||
import '../../presentation/cubits/login_cubit.dart';
|
||||
import '../../presentation/bloc/auth_bloc.dart';
|
||||
import '../../presentation/bloc/auth_event.dart';
|
||||
import '../../data/auth_repository.dart';
|
||||
|
||||
class LoginPasswordScreen extends StatefulWidget {
|
||||
const LoginPasswordScreen({super.key});
|
||||
class LoginPasswordScreen extends StatelessWidget {
|
||||
final String? email;
|
||||
|
||||
const LoginPasswordScreen({super.key, this.email});
|
||||
|
||||
@override
|
||||
State<LoginPasswordScreen> createState() => _LoginPasswordScreenState();
|
||||
Widget build(BuildContext context) {
|
||||
final emailFromExtra = GoRouterState.of(context).extra as String?;
|
||||
final initialEmail = email ?? emailFromExtra ?? '';
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
final cubit = LoginCubit(context.read<AuthRepository>());
|
||||
if (initialEmail.isNotEmpty) {
|
||||
cubit.emailChanged(initialEmail);
|
||||
}
|
||||
return cubit;
|
||||
},
|
||||
child: LoginPasswordView(initialEmail: initialEmail),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoginPasswordScreenState extends State<LoginPasswordScreen> {
|
||||
final _passwordController = TextEditingController();
|
||||
class LoginPasswordView extends StatefulWidget {
|
||||
final String initialEmail;
|
||||
|
||||
const LoginPasswordView({super.key, required this.initialEmail});
|
||||
|
||||
@override
|
||||
State<LoginPasswordView> createState() => _LoginPasswordViewState();
|
||||
}
|
||||
|
||||
class _LoginPasswordViewState extends State<LoginPasswordView> {
|
||||
late final TextEditingController _passwordController;
|
||||
bool _obscureText = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_passwordController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
final cubit = context.read<LoginCubit>();
|
||||
cubit.passwordChanged(_passwordController.text);
|
||||
|
||||
if (!cubit.state.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
final response = await cubit.submit();
|
||||
if (response != null && mounted) {
|
||||
context.read<AuthBloc>().add(AuthLoggedIn(user: response.user));
|
||||
context.go('/home');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -92,54 +145,62 @@ class _LoginPasswordScreenState extends State<LoginPasswordScreen> {
|
||||
}
|
||||
|
||||
Widget _buildFormContainer() {
|
||||
return SizedBox(
|
||||
width: 327,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return BlocBuilder<LoginCubit, LoginState>(
|
||||
builder: (context, state) {
|
||||
return SizedBox(
|
||||
width: 327,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'密码',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscureText,
|
||||
decoration: InputDecoration(
|
||||
hintText: '请输入密码',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureText ? Icons.visibility_off : Icons.visibility,
|
||||
size: 20,
|
||||
color: AppColors.slate400,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'密码',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscureText,
|
||||
decoration: InputDecoration(
|
||||
hintText: '请输入密码',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureText
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
size: 20,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (state.errorMessage != null)
|
||||
WarningBanner(message: state.errorMessage!, visible: true),
|
||||
const SizedBox(height: 12),
|
||||
AppButton(
|
||||
text: '登录',
|
||||
onPressed: state.status == FormzSubmissionStatus.inProgress
|
||||
? null
|
||||
: _handleLogin,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AppButton(text: '登录', onPressed: () => context.go('/home')),
|
||||
const SizedBox(height: 12),
|
||||
AppButton(
|
||||
text: '使用验证码登录',
|
||||
isOutlined: true,
|
||||
onPressed: () => context.push('/login/code'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:formz/formz.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/warning_banner.dart';
|
||||
import '../../presentation/cubits/register_cubit.dart';
|
||||
import '../../data/auth_repository.dart';
|
||||
|
||||
class RegisterScreen extends StatefulWidget {
|
||||
class RegisterScreen extends StatelessWidget {
|
||||
const RegisterScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RegisterScreen> createState() => _RegisterScreenState();
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => RegisterCubit(context.read<AuthRepository>()),
|
||||
child: const RegisterView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RegisterScreenState extends State<RegisterScreen> {
|
||||
class RegisterView extends StatefulWidget {
|
||||
const RegisterView({super.key});
|
||||
|
||||
@override
|
||||
State<RegisterView> createState() => _RegisterViewState();
|
||||
}
|
||||
|
||||
class _RegisterViewState extends State<RegisterView> {
|
||||
final _nicknameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _obscureText = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nicknameController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleNext() async {
|
||||
final cubit = context.read<RegisterCubit>();
|
||||
cubit.usernameChanged(_nicknameController.text);
|
||||
cubit.emailChanged(_emailController.text);
|
||||
cubit.passwordChanged(_passwordController.text);
|
||||
|
||||
if (!cubit.state.isStep1Valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await cubit.submitStep1();
|
||||
if (success && mounted) {
|
||||
context.push('/register/step2', extra: cubit);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -93,23 +129,39 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
}
|
||||
|
||||
Widget _buildFormContainer() {
|
||||
return SizedBox(
|
||||
width: 327,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildInput('昵称', '请输入昵称', _nicknameController),
|
||||
const SizedBox(height: 12),
|
||||
_buildInput('邮箱', '请输入邮箱', _emailController),
|
||||
const SizedBox(height: 12),
|
||||
_buildStepIndicator(),
|
||||
const SizedBox(height: 12),
|
||||
AppButton(
|
||||
text: '下一步',
|
||||
onPressed: () => context.push('/register/step2'),
|
||||
return BlocBuilder<RegisterCubit, RegisterState>(
|
||||
builder: (context, state) {
|
||||
return SizedBox(
|
||||
width: 327,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildInput('昵称', '请输入昵称', _nicknameController),
|
||||
const SizedBox(height: 12),
|
||||
_buildInput('邮箱', '请输入邮箱', _emailController),
|
||||
const SizedBox(height: 12),
|
||||
_buildPasswordInput(),
|
||||
const SizedBox(height: 12),
|
||||
_buildStepIndicator(),
|
||||
if (state.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: WarningBanner(
|
||||
message: state.errorMessage!,
|
||||
visible: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AppButton(
|
||||
text: '下一步',
|
||||
onPressed: state.status == FormzSubmissionStatus.inProgress
|
||||
? null
|
||||
: _handleNext,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,6 +190,42 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordInput() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'密码',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscureText,
|
||||
decoration: InputDecoration(
|
||||
hintText: '请输入至少 6 位密码',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureText ? Icons.visibility_off : Icons.visibility,
|
||||
size: 20,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepIndicator() {
|
||||
return Row(
|
||||
children: [
|
||||
|
||||
@@ -1,29 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:formz/formz.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/warning_banner.dart';
|
||||
import '../../presentation/cubits/register_cubit.dart';
|
||||
import '../../presentation/bloc/auth_bloc.dart';
|
||||
import '../../presentation/bloc/auth_event.dart';
|
||||
|
||||
class RegisterStep2Screen extends StatefulWidget {
|
||||
const RegisterStep2Screen({super.key});
|
||||
class RegisterStep2Screen extends StatelessWidget {
|
||||
final RegisterCubit? cubit;
|
||||
|
||||
const RegisterStep2Screen({super.key, this.cubit});
|
||||
|
||||
@override
|
||||
State<RegisterStep2Screen> createState() => _RegisterStep2ScreenState();
|
||||
Widget build(BuildContext context) {
|
||||
final registerCubit =
|
||||
cubit ?? (GoRouterState.of(context).extra as RegisterCubit?);
|
||||
|
||||
if (registerCubit == null) {
|
||||
return Scaffold(
|
||||
body: Center(child: Text('Error: RegisterCubit not found')),
|
||||
);
|
||||
}
|
||||
|
||||
return BlocProvider.value(
|
||||
value: registerCubit,
|
||||
child: const RegisterStep2View(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RegisterStep2ScreenState extends State<RegisterStep2Screen> {
|
||||
final _passwordController = TextEditingController();
|
||||
class RegisterStep2View extends StatefulWidget {
|
||||
const RegisterStep2View({super.key});
|
||||
|
||||
@override
|
||||
State<RegisterStep2View> createState() => _RegisterStep2ViewState();
|
||||
}
|
||||
|
||||
class _RegisterStep2ViewState extends State<RegisterStep2View> {
|
||||
final _codeController = TextEditingController();
|
||||
final _inviteController = TextEditingController();
|
||||
bool _obscureText = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_passwordController.dispose();
|
||||
_codeController.dispose();
|
||||
_inviteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleComplete() async {
|
||||
final cubit = context.read<RegisterCubit>();
|
||||
cubit.verificationCodeChanged(_codeController.text);
|
||||
|
||||
if (!cubit.state.isStep2Valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
final response = await cubit.submitStep2();
|
||||
if (response != null && mounted) {
|
||||
context.read<AuthBloc>().add(AuthLoggedIn(user: response.user));
|
||||
context.go('/home');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleResendCode() async {
|
||||
await context.read<RegisterCubit>().resendCode();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -96,62 +141,41 @@ class _RegisterStep2ScreenState extends State<RegisterStep2Screen> {
|
||||
}
|
||||
|
||||
Widget _buildFormContainer() {
|
||||
return SizedBox(
|
||||
width: 327,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPasswordInput(),
|
||||
const SizedBox(height: 12),
|
||||
_buildCodeInput(),
|
||||
const SizedBox(height: 12),
|
||||
_buildInviteInput(),
|
||||
const SizedBox(height: 12),
|
||||
_buildStepIndicator(),
|
||||
const SizedBox(height: 12),
|
||||
AppButton(text: '完成注册', onPressed: () {}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordInput() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'密码',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscureText,
|
||||
decoration: InputDecoration(
|
||||
hintText: '请输入至少 8 位密码',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureText ? Icons.visibility_off : Icons.visibility,
|
||||
size: 20,
|
||||
color: AppColors.slate400,
|
||||
return BlocBuilder<RegisterCubit, RegisterState>(
|
||||
builder: (context, state) {
|
||||
return SizedBox(
|
||||
width: 327,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildCodeInput(state),
|
||||
const SizedBox(height: 12),
|
||||
_buildInviteInput(),
|
||||
const SizedBox(height: 12),
|
||||
_buildStepIndicator(),
|
||||
if (state.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: WarningBanner(
|
||||
message: state.errorMessage!,
|
||||
visible: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AppButton(
|
||||
text: '完成注册',
|
||||
onPressed: state.status == FormzSubmissionStatus.inProgress
|
||||
? null
|
||||
: _handleComplete,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCodeInput() {
|
||||
Widget _buildCodeInput(RegisterState state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -187,7 +211,9 @@ class _RegisterStep2ScreenState extends State<RegisterStep2Screen> {
|
||||
width: 112,
|
||||
height: 40,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {},
|
||||
onPressed: state.status == FormzSubmissionStatus.inProgress
|
||||
? null
|
||||
: _handleResendCode,
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: AppColors.background,
|
||||
side: const BorderSide(color: AppColors.input),
|
||||
|
||||
Reference in New Issue
Block a user