Merge pull request #366 from Oloodi/357-staff-app-users-get-stuck-in-otp-error-loop-when-signing-up-with-existing-account
357 staff app users get stuck in otp error loop when signing up with existing account
This commit is contained in:
@@ -18,6 +18,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
|
|
||||||
final FirebaseAuth firebaseAuth;
|
final FirebaseAuth firebaseAuth;
|
||||||
final ExampleConnector dataConnect;
|
final ExampleConnector dataConnect;
|
||||||
|
Completer<String?>? _pendingVerification;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<domain.User?> get currentUser => firebaseAuth
|
Stream<domain.User?> get currentUser => firebaseAuth
|
||||||
@@ -39,6 +40,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
@override
|
@override
|
||||||
Future<String?> signInWithPhone({required String phoneNumber}) async {
|
Future<String?> signInWithPhone({required String phoneNumber}) async {
|
||||||
final Completer<String?> completer = Completer<String?>();
|
final Completer<String?> completer = Completer<String?>();
|
||||||
|
_pendingVerification = completer;
|
||||||
|
|
||||||
await firebaseAuth.verifyPhoneNumber(
|
await firebaseAuth.verifyPhoneNumber(
|
||||||
phoneNumber: phoneNumber,
|
phoneNumber: phoneNumber,
|
||||||
@@ -76,6 +78,15 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void cancelPendingPhoneVerification() {
|
||||||
|
final Completer<String?>? completer = _pendingVerification;
|
||||||
|
if (completer != null && !completer.isCompleted) {
|
||||||
|
completer.completeError(Exception('Phone verification cancelled.'));
|
||||||
|
}
|
||||||
|
_pendingVerification = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Signs out the current user.
|
/// Signs out the current user.
|
||||||
@override
|
@override
|
||||||
Future<void> signOut() {
|
Future<void> signOut() {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ abstract interface class AuthRepositoryInterface {
|
|||||||
/// Signs in with a phone number and returns a verification ID.
|
/// Signs in with a phone number and returns a verification ID.
|
||||||
Future<String?> signInWithPhone({required String phoneNumber});
|
Future<String?> signInWithPhone({required String phoneNumber});
|
||||||
|
|
||||||
|
/// Cancels any pending phone verification request (if possible).
|
||||||
|
void cancelPendingPhoneVerification();
|
||||||
|
|
||||||
/// Verifies the OTP code and returns the authenticated user.
|
/// Verifies the OTP code and returns the authenticated user.
|
||||||
Future<User?> verifyOtp({
|
Future<User?> verifyOtp({
|
||||||
required String verificationId,
|
required String verificationId,
|
||||||
|
|||||||
@@ -18,4 +18,8 @@ class SignInWithPhoneUseCase
|
|||||||
Future<String?> call(SignInWithPhoneArguments arguments) {
|
Future<String?> call(SignInWithPhoneArguments arguments) {
|
||||||
return _repository.signInWithPhone(phoneNumber: arguments.phoneNumber);
|
return _repository.signInWithPhone(phoneNumber: arguments.phoneNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void cancelPending() {
|
||||||
|
_repository.cancelPendingPhoneVerification();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
@@ -15,6 +16,11 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
|
|
||||||
/// The use case for verifying an OTP.
|
/// The use case for verifying an OTP.
|
||||||
final VerifyOtpUseCase _verifyOtpUseCase;
|
final VerifyOtpUseCase _verifyOtpUseCase;
|
||||||
|
int _requestToken = 0;
|
||||||
|
DateTime? _lastCodeRequestAt;
|
||||||
|
DateTime? _cooldownUntil;
|
||||||
|
static const Duration _resendCooldown = Duration(seconds: 31);
|
||||||
|
Timer? _cooldownTimer;
|
||||||
|
|
||||||
/// Creates an [AuthBloc].
|
/// Creates an [AuthBloc].
|
||||||
AuthBloc({
|
AuthBloc({
|
||||||
@@ -28,11 +34,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
on<AuthErrorCleared>(_onErrorCleared);
|
on<AuthErrorCleared>(_onErrorCleared);
|
||||||
on<AuthOtpUpdated>(_onOtpUpdated);
|
on<AuthOtpUpdated>(_onOtpUpdated);
|
||||||
on<AuthPhoneUpdated>(_onPhoneUpdated);
|
on<AuthPhoneUpdated>(_onPhoneUpdated);
|
||||||
|
on<AuthResetRequested>(_onResetRequested);
|
||||||
|
on<AuthCooldownTicked>(_onCooldownTicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears any authentication error from the state.
|
/// Clears any authentication error from the state.
|
||||||
void _onErrorCleared(AuthErrorCleared event, Emitter<AuthState> emit) {
|
void _onErrorCleared(AuthErrorCleared event, Emitter<AuthState> emit) {
|
||||||
emit(state.copyWith(status: AuthStatus.codeSent, errorMessage: null));
|
emit(state.copyWith(status: AuthStatus.codeSent, errorMessage: ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the internal OTP state without triggering a submission.
|
/// Updates the internal OTP state without triggering a submission.
|
||||||
@@ -41,14 +49,22 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
otp: event.otp,
|
otp: event.otp,
|
||||||
status: AuthStatus.codeSent,
|
status: AuthStatus.codeSent,
|
||||||
errorMessage: null,
|
errorMessage: '',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the internal phone number state without triggering a submission.
|
/// Updates the internal phone number state without triggering a submission.
|
||||||
void _onPhoneUpdated(AuthPhoneUpdated event, Emitter<AuthState> emit) {
|
void _onPhoneUpdated(AuthPhoneUpdated event, Emitter<AuthState> emit) {
|
||||||
emit(state.copyWith(phoneNumber: event.phoneNumber, errorMessage: null));
|
emit(state.copyWith(phoneNumber: event.phoneNumber, errorMessage: ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the authentication state to initial for a given mode.
|
||||||
|
void _onResetRequested(AuthResetRequested event, Emitter<AuthState> emit) {
|
||||||
|
_requestToken++;
|
||||||
|
_signInUseCase.cancelPending();
|
||||||
|
_cancelCooldownTimer();
|
||||||
|
emit(AuthState(status: AuthStatus.initial, mode: event.mode));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the sign-in request, initiating the phone authentication process.
|
/// Handles the sign-in request, initiating the phone authentication process.
|
||||||
@@ -56,11 +72,37 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
AuthSignInRequested event,
|
AuthSignInRequested event,
|
||||||
Emitter<AuthState> emit,
|
Emitter<AuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
|
if (_lastCodeRequestAt != null) {
|
||||||
|
final DateTime cooldownUntil =
|
||||||
|
_cooldownUntil ?? _lastCodeRequestAt!.add(_resendCooldown);
|
||||||
|
final int remaining = cooldownUntil.difference(now).inSeconds;
|
||||||
|
if (remaining > 0) {
|
||||||
|
_startCooldown(remaining);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: AuthStatus.error,
|
||||||
|
mode: event.mode,
|
||||||
|
phoneNumber: event.phoneNumber ?? state.phoneNumber,
|
||||||
|
errorMessage: 'Please wait ${remaining}s before requesting a new code.',
|
||||||
|
cooldownSecondsRemaining: remaining,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_signInUseCase.cancelPending();
|
||||||
|
final int token = ++_requestToken;
|
||||||
|
_lastCodeRequestAt = now;
|
||||||
|
_cooldownUntil = now.add(_resendCooldown);
|
||||||
|
_cancelCooldownTimer();
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: AuthStatus.loading,
|
status: AuthStatus.loading,
|
||||||
mode: event.mode,
|
mode: event.mode,
|
||||||
phoneNumber: event.phoneNumber,
|
phoneNumber: event.phoneNumber,
|
||||||
|
cooldownSecondsRemaining: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
@@ -69,19 +111,79 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
phoneNumber: event.phoneNumber ?? state.phoneNumber,
|
phoneNumber: event.phoneNumber ?? state.phoneNumber,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (token != _requestToken) return;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: AuthStatus.codeSent,
|
status: AuthStatus.codeSent,
|
||||||
verificationId: verificationId,
|
verificationId: verificationId,
|
||||||
|
cooldownSecondsRemaining: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (token != _requestToken) return;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(status: AuthStatus.error, errorMessage: e.toString()),
|
state.copyWith(
|
||||||
|
status: AuthStatus.error,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
cooldownSecondsRemaining: 0,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onCooldownTicked(
|
||||||
|
AuthCooldownTicked event,
|
||||||
|
Emitter<AuthState> emit,
|
||||||
|
) {
|
||||||
|
print('Auth cooldown tick: ${event.secondsRemaining}');
|
||||||
|
if (event.secondsRemaining <= 0) {
|
||||||
|
print('Auth cooldown finished: clearing message');
|
||||||
|
_cancelCooldownTimer();
|
||||||
|
_cooldownUntil = null;
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: AuthStatus.initial,
|
||||||
|
errorMessage: '',
|
||||||
|
cooldownSecondsRemaining: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: AuthStatus.error,
|
||||||
|
errorMessage:
|
||||||
|
'Please wait ${event.secondsRemaining}s before requesting a new code.',
|
||||||
|
cooldownSecondsRemaining: event.secondsRemaining,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startCooldown(int secondsRemaining) {
|
||||||
|
_cancelCooldownTimer();
|
||||||
|
int remaining = secondsRemaining;
|
||||||
|
add(AuthCooldownTicked(remaining));
|
||||||
|
_cooldownTimer = Timer.periodic(const Duration(seconds: 1), (Timer timer) {
|
||||||
|
remaining -= 1;
|
||||||
|
print('Auth cooldown timer: remaining=$remaining');
|
||||||
|
if (remaining <= 0) {
|
||||||
|
timer.cancel();
|
||||||
|
_cooldownTimer = null;
|
||||||
|
print('Auth cooldown timer: reached 0, emitting tick');
|
||||||
|
add(const AuthCooldownTicked(0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
add(AuthCooldownTicked(remaining));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelCooldownTimer() {
|
||||||
|
_cooldownTimer?.cancel();
|
||||||
|
_cooldownTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handles OTP submission and verification.
|
/// Handles OTP submission and verification.
|
||||||
Future<void> _onOtpSubmitted(
|
Future<void> _onOtpSubmitted(
|
||||||
AuthOtpSubmitted event,
|
AuthOtpSubmitted event,
|
||||||
@@ -107,6 +209,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
/// Disposes the BLoC resources.
|
/// Disposes the BLoC resources.
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_cancelCooldownTimer();
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,27 @@ class AuthOtpSubmitted extends AuthEvent {
|
|||||||
/// Event for clearing any authentication error in the state.
|
/// Event for clearing any authentication error in the state.
|
||||||
class AuthErrorCleared extends AuthEvent {}
|
class AuthErrorCleared extends AuthEvent {}
|
||||||
|
|
||||||
|
/// Event for resetting the authentication flow back to initial.
|
||||||
|
class AuthResetRequested extends AuthEvent {
|
||||||
|
/// The authentication mode (login or signup).
|
||||||
|
final AuthMode mode;
|
||||||
|
|
||||||
|
const AuthResetRequested({required this.mode});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => <Object>[mode];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event for ticking down the resend cooldown.
|
||||||
|
class AuthCooldownTicked extends AuthEvent {
|
||||||
|
final int secondsRemaining;
|
||||||
|
|
||||||
|
const AuthCooldownTicked(this.secondsRemaining);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => <Object>[secondsRemaining];
|
||||||
|
}
|
||||||
|
|
||||||
/// Event for updating the current draft OTP in the state.
|
/// Event for updating the current draft OTP in the state.
|
||||||
class AuthOtpUpdated extends AuthEvent {
|
class AuthOtpUpdated extends AuthEvent {
|
||||||
/// The current draft OTP.
|
/// The current draft OTP.
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ class AuthState extends Equatable {
|
|||||||
/// A descriptive message for any error that occurred.
|
/// A descriptive message for any error that occurred.
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// Cooldown in seconds before requesting a new code.
|
||||||
|
final int cooldownSecondsRemaining;
|
||||||
|
|
||||||
/// The authenticated user's data (available when status is [AuthStatus.authenticated]).
|
/// The authenticated user's data (available when status is [AuthStatus.authenticated]).
|
||||||
final User? user;
|
final User? user;
|
||||||
|
|
||||||
@@ -50,6 +53,7 @@ class AuthState extends Equatable {
|
|||||||
this.otp = '',
|
this.otp = '',
|
||||||
this.phoneNumber = '',
|
this.phoneNumber = '',
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.cooldownSecondsRemaining = 0,
|
||||||
this.user,
|
this.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,6 +65,7 @@ class AuthState extends Equatable {
|
|||||||
otp,
|
otp,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
cooldownSecondsRemaining,
|
||||||
user,
|
user,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -78,6 +83,7 @@ class AuthState extends Equatable {
|
|||||||
String? otp,
|
String? otp,
|
||||||
String? phoneNumber,
|
String? phoneNumber,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
int? cooldownSecondsRemaining,
|
||||||
User? user,
|
User? user,
|
||||||
}) {
|
}) {
|
||||||
return AuthState(
|
return AuthState(
|
||||||
@@ -87,6 +93,8 @@ class AuthState extends Equatable {
|
|||||||
otp: otp ?? this.otp,
|
otp: otp ?? this.otp,
|
||||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
cooldownSecondsRemaining:
|
||||||
|
cooldownSecondsRemaining ?? this.cooldownSecondsRemaining,
|
||||||
user: user ?? this.user,
|
user: user ?? this.user,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,23 +15,46 @@ import '../widgets/phone_verification_page/phone_input.dart';
|
|||||||
///
|
///
|
||||||
/// This page coordinates the authentication flow by switching between
|
/// This page coordinates the authentication flow by switching between
|
||||||
/// [PhoneInput] and [OtpVerification] based on the current [AuthState].
|
/// [PhoneInput] and [OtpVerification] based on the current [AuthState].
|
||||||
class PhoneVerificationPage extends StatelessWidget {
|
class PhoneVerificationPage extends StatefulWidget {
|
||||||
/// The authentication mode (login or signup).
|
/// The authentication mode (login or signup).
|
||||||
final AuthMode mode;
|
final AuthMode mode;
|
||||||
|
|
||||||
/// Creates a [PhoneVerificationPage].
|
/// Creates a [PhoneVerificationPage].
|
||||||
const PhoneVerificationPage({super.key, required this.mode});
|
const PhoneVerificationPage({super.key, required this.mode});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PhoneVerificationPage> createState() => _PhoneVerificationPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
||||||
|
late final AuthBloc _authBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_authBloc = Modular.get<AuthBloc>();
|
||||||
|
_authBloc.add(AuthResetRequested(mode: widget.mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_authBloc.add(AuthResetRequested(mode: widget.mode));
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles the request to send a verification code to the provided phone number.
|
/// Handles the request to send a verification code to the provided phone number.
|
||||||
void _onSendCode({
|
void _onSendCode({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String phoneNumber,
|
required String phoneNumber,
|
||||||
}) {
|
}) {
|
||||||
print('Phone verification input: "$phoneNumber" len=${phoneNumber.length}');
|
final String normalized = phoneNumber.replaceAll(RegExp(r'\\D'), '');
|
||||||
if (phoneNumber.length == 10) {
|
print('Phone verification input: "$normalized" len=${normalized.length}');
|
||||||
|
if (normalized.length == 10) {
|
||||||
BlocProvider.of<AuthBloc>(
|
BlocProvider.of<AuthBloc>(
|
||||||
context,
|
context,
|
||||||
).add(AuthSignInRequested(phoneNumber: '+1$phoneNumber', mode: mode));
|
).add(
|
||||||
|
AuthSignInRequested(phoneNumber: '+1$normalized', mode: widget.mode),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -55,20 +78,22 @@ class PhoneVerificationPage extends StatelessWidget {
|
|||||||
AuthOtpSubmitted(
|
AuthOtpSubmitted(
|
||||||
verificationId: verificationId,
|
verificationId: verificationId,
|
||||||
smsCode: otp,
|
smsCode: otp,
|
||||||
mode: mode,
|
mode: widget.mode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the request to resend the verification code using the phone number in the state.
|
/// Handles the request to resend the verification code using the phone number in the state.
|
||||||
void _onResend({required BuildContext context}) {
|
void _onResend({required BuildContext context}) {
|
||||||
BlocProvider.of<AuthBloc>(context).add(AuthSignInRequested(mode: mode));
|
BlocProvider.of<AuthBloc>(context).add(
|
||||||
|
AuthSignInRequested(mode: widget.mode),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<AuthBloc>(
|
return BlocProvider<AuthBloc>.value(
|
||||||
create: (BuildContext context) => Modular.get<AuthBloc>(),
|
value: _authBloc,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return BlocListener<AuthBloc, AuthState>(
|
return BlocListener<AuthBloc, AuthState>(
|
||||||
@@ -83,12 +108,19 @@ class PhoneVerificationPage extends StatelessWidget {
|
|||||||
state.mode == AuthMode.signup) {
|
state.mode == AuthMode.signup) {
|
||||||
final String message = state.errorMessage ?? '';
|
final String message = state.errorMessage ?? '';
|
||||||
if (message.contains('staff profile')) {
|
if (message.contains('staff profile')) {
|
||||||
Modular.to.pushReplacementNamed(
|
final ScaffoldMessengerState messenger =
|
||||||
'./phone-verification',
|
ScaffoldMessenger.of(context);
|
||||||
arguments: <String, String>{
|
messenger.hideCurrentSnackBar();
|
||||||
'mode': AuthMode.login.name,
|
messenger.showSnackBar(
|
||||||
},
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 5),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
Future<void>.delayed(const Duration(seconds: 5), () {
|
||||||
|
if (!mounted) return;
|
||||||
|
Modular.to.navigate('/');
|
||||||
|
});
|
||||||
} else if (message.contains('not authorized')) {
|
} else if (message.contains('not authorized')) {
|
||||||
Modular.to.pop();
|
Modular.to.pop();
|
||||||
}
|
}
|
||||||
@@ -104,10 +136,23 @@ class PhoneVerificationPage extends StatelessWidget {
|
|||||||
(state.status == AuthStatus.loading &&
|
(state.status == AuthStatus.loading &&
|
||||||
state.verificationId != null);
|
state.verificationId != null);
|
||||||
|
|
||||||
return Scaffold(
|
return WillPopScope(
|
||||||
appBar: const UiAppBar(
|
onWillPop: () async {
|
||||||
|
BlocProvider.of<AuthBloc>(
|
||||||
|
context,
|
||||||
|
).add(AuthResetRequested(mode: widget.mode));
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: UiAppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
|
onLeadingPressed: () {
|
||||||
|
BlocProvider.of<AuthBloc>(context).add(
|
||||||
|
AuthResetRequested(mode: widget.mode),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: isOtpStep
|
child: isOtpStep
|
||||||
@@ -127,9 +172,10 @@ class PhoneVerificationPage extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: PhoneInput(
|
: PhoneInput(
|
||||||
state: state,
|
state: state,
|
||||||
onSendCode: () => _onSendCode(
|
onSendCode: (String phoneNumber) => _onSendCode(
|
||||||
context: context,
|
context: context,
|
||||||
phoneNumber: state.phoneNumber,
|
phoneNumber: phoneNumber,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,16 +17,25 @@ class PhoneInput extends StatefulWidget {
|
|||||||
final AuthState state;
|
final AuthState state;
|
||||||
|
|
||||||
/// Callback for when the "Send Code" action is triggered.
|
/// Callback for when the "Send Code" action is triggered.
|
||||||
final VoidCallback onSendCode;
|
final ValueChanged<String> onSendCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PhoneInput> createState() => _PhoneInputState();
|
State<PhoneInput> createState() => _PhoneInputState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PhoneInputState extends State<PhoneInput> {
|
class _PhoneInputState extends State<PhoneInput> {
|
||||||
|
String _currentPhone = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentPhone = widget.state.phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
void _handlePhoneChanged(String value) {
|
void _handlePhoneChanged(String value) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
_currentPhone = value;
|
||||||
final AuthBloc bloc = context.read<AuthBloc>();
|
final AuthBloc bloc = context.read<AuthBloc>();
|
||||||
if (!bloc.isClosed) {
|
if (!bloc.isClosed) {
|
||||||
bloc.add(AuthPhoneUpdated(value));
|
bloc.add(AuthPhoneUpdated(value));
|
||||||
@@ -59,7 +68,7 @@ class _PhoneInputState extends State<PhoneInput> {
|
|||||||
),
|
),
|
||||||
PhoneInputActions(
|
PhoneInputActions(
|
||||||
isLoading: widget.state.isLoading,
|
isLoading: widget.state.isLoading,
|
||||||
onSendCode: widget.onSendCode,
|
onSendCode: () => widget.onSendCode(_currentPhone),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ class _PhoneInputFormFieldState extends State<PhoneInputFormField> {
|
|||||||
_controller = TextEditingController(text: widget.initialValue);
|
_controller = TextEditingController(text: widget.initialValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(PhoneInputFormField oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.initialValue != oldWidget.initialValue &&
|
||||||
|
_controller.text != widget.initialValue) {
|
||||||
|
_controller.text = widget.initialValue;
|
||||||
|
_controller.selection = TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: _controller.text.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
|
|||||||
Reference in New Issue
Block a user