feat: SMS OTP phone auth + mandatory phone verification gate

- MyUser: add isPhoneVerified field (from is_phone_verified in API)
- ApiUserRepository: signInWithPhoneNumber calls POST /auth/send-otp,
  verifyOTP calls POST /auth/phone, linkWithOTP calls POST /auth/verify-phone
  Added updateFcmToken and changePassword methods
- AuthBloc: remove Firebase PhoneVerificationService, use repository OTP flow
- app_view.dart: gate authenticated users without verified phone to
  PhoneVerifyRequiredScreen before showing HomeScreen
- welcome_screen.dart: full redesign with gradient header, logo, integrated
  OTP flow (phone → code in same screen), 60s resend timer
- PhoneVerifyRequiredScreen: new screen for users who need to verify phone

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-25 15:53:43 -05:00
co-authored by Claude Sonnet 4.6
parent 38beca4b4f
commit 2c5b237890
6 changed files with 564 additions and 186 deletions
@@ -13,6 +13,7 @@ class MyUser extends Equatable {
final String? gender;
final ProState proState;
final String? token;
final bool isPhoneVerified;
const MyUser({
required this.id,
@@ -26,6 +27,7 @@ class MyUser extends Equatable {
this.gender,
required this.proState,
this.token,
this.isPhoneVerified = false,
});
get drawerLabel => email != null && email!.isNotEmpty
@@ -47,6 +49,7 @@ class MyUser extends Equatable {
gender: '',
proState: ProState.inactive,
token: '',
isPhoneVerified: false,
);
/// Modify MyUser parameters
@@ -62,6 +65,7 @@ class MyUser extends Equatable {
String? gender,
ProState? proState,
String? token,
bool? isPhoneVerified,
}) {
return MyUser(
id: id ?? this.id,
@@ -75,6 +79,7 @@ class MyUser extends Equatable {
gender: gender ?? this.gender,
proState: proState ?? this.proState,
token: token ?? this.token,
isPhoneVerified: isPhoneVerified ?? this.isPhoneVerified,
);
}
@@ -129,5 +134,6 @@ class MyUser extends Equatable {
gender,
proState,
token,
isPhoneVerified,
];
}
@@ -69,6 +69,7 @@ class ApiUserRepository implements UserRepository {
gender: json['gender']?.toString(),
proState: _proStateFromInt((json['pro_state'] as num?)?.toInt() ?? 0),
token: null,
isPhoneVerified: json['is_phone_verified'] as bool? ?? false,
);
}
@@ -117,9 +118,7 @@ class ApiUserRepository implements UserRepository {
_controller.add(user);
}
@override
Future<void> signIn(String email, String password) async {
final data = await _post('/auth/login', {'email': email, 'password': password});
Future<void> _saveTokenAndEmit(Map<String, dynamic> data) async {
_token = data['access_token'] as String?;
if (_token != null) {
final prefs = await SharedPreferences.getInstance();
@@ -129,6 +128,12 @@ class ApiUserRepository implements UserRepository {
_emit(_current);
}
@override
Future<void> signIn(String email, String password) async {
final data = await _post('/auth/login', {'email': email, 'password': password});
await _saveTokenAndEmit(data);
}
@override
Future<MyUser> signUp(MyUser myUser, String password) async {
final data = await _post('/auth/register', {
@@ -136,13 +141,7 @@ class ApiUserRepository implements UserRepository {
'password': password,
'name': myUser.name ?? myUser.email ?? '',
});
_token = data['access_token'] as String?;
if (_token != null) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', _token!);
}
_current = _fromApi(data['user'] as Map<String, dynamic>);
_emit(_current);
await _saveTokenAndEmit(data);
return _current!;
}
@@ -270,13 +269,7 @@ class ApiUserRepository implements UserRepository {
'phone': _pendingOtpPhone!,
'code': code,
});
_token = data['access_token'] as String?;
if (_token != null) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', _token!);
}
_current = _fromApi(data['user'] as Map<String, dynamic>);
_emit(_current);
await _saveTokenAndEmit(data);
_pendingOtpPhone = null;
return true;
} catch (_) {
@@ -309,4 +302,22 @@ class ApiUserRepository implements UserRepository {
Future<void> resetPassword(String email) async {
// Not supported by current API — stub
}
Future<void> updateFcmToken(String token) async {
try {
await _post('/users/me/fcm-token', {'token': token}, auth: true);
} catch (_) {}
}
Future<bool> changePassword(String currentPassword, String newPassword) async {
try {
await _post('/auth/change-password', {
'current_password': currentPassword,
'new_password': newPassword,
}, auth: true);
return true;
} catch (_) {
return false;
}
}
}