feat(apps): add RegisterCubit for signup form
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:formz/formz.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../data/auth_repository.dart';
|
||||
import '../../data/models/signup_request.dart';
|
||||
import '../../data/models/auth_response.dart';
|
||||
|
||||
class Username extends FormzInput<String, String> {
|
||||
const Username.pure() : super.pure('');
|
||||
const Username.dirty([super.value = '']) : super.dirty();
|
||||
|
||||
@override
|
||||
String? validator(String value) {
|
||||
if (value.isEmpty) return 'Username is required';
|
||||
if (value.length < 3) return 'Username must be at least 3 characters';
|
||||
if (value.length > 30) return 'Username must be at most 30 characters';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class Email extends FormzInput<String, String> {
|
||||
const Email.pure() : super.pure('');
|
||||
const Email.dirty([super.value = '']) : super.dirty();
|
||||
|
||||
static final _regex = RegExp(r'^[\w.-]+@[\w.-]+\.\w+$');
|
||||
|
||||
@override
|
||||
String? validator(String value) {
|
||||
if (value.isEmpty) return 'Email is required';
|
||||
if (!_regex.hasMatch(value)) return 'Invalid email format';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class Password extends FormzInput<String, String> {
|
||||
const Password.pure() : super.pure('');
|
||||
const Password.dirty([super.value = '']) : super.dirty();
|
||||
|
||||
@override
|
||||
String? validator(String value) {
|
||||
if (value.isEmpty) return 'Password is required';
|
||||
if (value.length < 6) return 'Password must be at least 6 characters';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class VerificationCode extends FormzInput<String, String> {
|
||||
const VerificationCode.pure() : super.pure('');
|
||||
const VerificationCode.dirty([super.value = '']) : super.dirty();
|
||||
|
||||
@override
|
||||
String? validator(String value) {
|
||||
if (value.isEmpty) return 'Code is required';
|
||||
if (!RegExp(r'^\d{6}$').hasMatch(value)) return 'Code must be 6 digits';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterState extends Equatable {
|
||||
final Username username;
|
||||
final Email email;
|
||||
final Password password;
|
||||
final VerificationCode verificationCode;
|
||||
final FormzSubmissionStatus status;
|
||||
final String? errorMessage;
|
||||
final String? pendingEmail;
|
||||
final bool codeSent;
|
||||
|
||||
const RegisterState({
|
||||
this.username = const Username.pure(),
|
||||
this.email = const Email.pure(),
|
||||
this.password = const Password.pure(),
|
||||
this.verificationCode = const VerificationCode.pure(),
|
||||
this.status = FormzSubmissionStatus.initial,
|
||||
this.errorMessage,
|
||||
this.pendingEmail,
|
||||
this.codeSent = false,
|
||||
});
|
||||
|
||||
bool get isStep1Valid =>
|
||||
username.isValid && email.isValid && password.isValid;
|
||||
bool get isStep2Valid => verificationCode.isValid;
|
||||
|
||||
RegisterState copyWith({
|
||||
Username? username,
|
||||
Email? email,
|
||||
Password? password,
|
||||
VerificationCode? verificationCode,
|
||||
FormzSubmissionStatus? status,
|
||||
String? errorMessage,
|
||||
String? pendingEmail,
|
||||
bool? codeSent,
|
||||
}) {
|
||||
return RegisterState(
|
||||
username: username ?? this.username,
|
||||
email: email ?? this.email,
|
||||
password: password ?? this.password,
|
||||
verificationCode: verificationCode ?? this.verificationCode,
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage,
|
||||
pendingEmail: pendingEmail ?? this.pendingEmail,
|
||||
codeSent: codeSent ?? this.codeSent,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
verificationCode,
|
||||
status,
|
||||
errorMessage,
|
||||
pendingEmail,
|
||||
codeSent,
|
||||
];
|
||||
}
|
||||
|
||||
class RegisterCubit extends Cubit<RegisterState> {
|
||||
final AuthRepository _repository;
|
||||
|
||||
RegisterCubit(this._repository) : super(const RegisterState());
|
||||
|
||||
void usernameChanged(String value) {
|
||||
emit(state.copyWith(username: Username.dirty(value)));
|
||||
}
|
||||
|
||||
void emailChanged(String value) {
|
||||
emit(state.copyWith(email: Email.dirty(value)));
|
||||
}
|
||||
|
||||
void passwordChanged(String value) {
|
||||
emit(state.copyWith(password: Password.dirty(value)));
|
||||
}
|
||||
|
||||
void verificationCodeChanged(String value) {
|
||||
emit(state.copyWith(verificationCode: VerificationCode.dirty(value)));
|
||||
}
|
||||
|
||||
Future<bool> submitStep1() async {
|
||||
if (!state.isStep1Valid) return false;
|
||||
|
||||
emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
|
||||
|
||||
try {
|
||||
final response = await _repository.signupStart(
|
||||
SignupStartRequest(
|
||||
username: state.username.value,
|
||||
email: state.email.value,
|
||||
password: state.password.value,
|
||||
),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.success,
|
||||
pendingEmail: response.email,
|
||||
codeSent: true,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<AuthResponse?> submitStep2() async {
|
||||
if (!state.isStep2Valid || state.pendingEmail == null) return null;
|
||||
|
||||
emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
|
||||
|
||||
try {
|
||||
final response = await _repository.signupVerify(
|
||||
SignupVerifyRequest(
|
||||
email: state.pendingEmail!,
|
||||
token: state.verificationCode.value,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(status: FormzSubmissionStatus.success));
|
||||
return response;
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resendCode() async {
|
||||
if (state.pendingEmail == null) return;
|
||||
|
||||
try {
|
||||
await _repository.signupResend(
|
||||
SignupResendRequest(email: state.pendingEmail!),
|
||||
);
|
||||
emit(state.copyWith(codeSent: true));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:formz/formz.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:social_app/features/auth/data/auth_repository.dart';
|
||||
import 'package:social_app/features/auth/presentation/cubits/register_cubit.dart';
|
||||
|
||||
class MockAuthRepository extends Mock implements AuthRepository {}
|
||||
|
||||
void main() {
|
||||
late RegisterCubit cubit;
|
||||
late MockAuthRepository mockRepository;
|
||||
|
||||
setUp(() {
|
||||
mockRepository = MockAuthRepository();
|
||||
cubit = RegisterCubit(mockRepository);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
cubit.close();
|
||||
});
|
||||
|
||||
group('RegisterCubit', () {
|
||||
test('initial state has pure status', () {
|
||||
expect(cubit.state.status, FormzSubmissionStatus.initial);
|
||||
});
|
||||
|
||||
blocTest<RegisterCubit, RegisterState>(
|
||||
'usernameChanged updates username',
|
||||
build: () => cubit,
|
||||
act: (c) => c.usernameChanged('testuser'),
|
||||
expect: () => [isA<RegisterState>()],
|
||||
);
|
||||
|
||||
blocTest<RegisterCubit, RegisterState>(
|
||||
'emailChanged updates email',
|
||||
build: () => cubit,
|
||||
act: (c) => c.emailChanged('test@example.com'),
|
||||
expect: () => [isA<RegisterState>()],
|
||||
);
|
||||
|
||||
blocTest<RegisterCubit, RegisterState>(
|
||||
'passwordChanged updates password',
|
||||
build: () => cubit,
|
||||
act: (c) => c.passwordChanged('password123'),
|
||||
expect: () => [isA<RegisterState>()],
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user