flagged bugs fix
This commit is contained in:
@@ -7,10 +7,6 @@ class ApiConstants {
|
||||
static const String verifyPin = '$baseUrl/customer/verify-pin';
|
||||
static const String resetPin = '$baseUrl/customer/reset-pin';
|
||||
static const String logout = '$baseUrl/customer/logout';
|
||||
|
||||
// ---- Email OTP (Replace Firebase) ----
|
||||
static const String sendEmailOtp = '$baseUrl/customer/send-email-otp';
|
||||
static const String verifyEmailOtp = '$baseUrl/customer/verify-email-otp';
|
||||
|
||||
// ---- Profile ----
|
||||
static const String profile = '$baseUrl/customer/profile';
|
||||
|
||||
@@ -34,32 +34,12 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
await state.initiateLogin(_phone.text);
|
||||
if (!mounted) return;
|
||||
|
||||
state.isLoginFlow = true;
|
||||
|
||||
// Bypass OTP for Easter Egg
|
||||
if (_phone.text.replaceAll(' ', '') == '9876543210') {
|
||||
setState(() => _loading = false);
|
||||
Navigator.pushNamed(context, Routes.enterPin);
|
||||
return;
|
||||
}
|
||||
|
||||
await state.sendOtp(
|
||||
_phone.text,
|
||||
onSuccess: () {
|
||||
if (!mounted) return;
|
||||
setState(() => _loading = false);
|
||||
Navigator.pushNamed(context, Routes.otp);
|
||||
},
|
||||
onError: (err) {
|
||||
if (!mounted) return;
|
||||
state.isLoginFlow = false;
|
||||
setState(() => _loading = false);
|
||||
SnackbarUtils.showError(context, err);
|
||||
},
|
||||
);
|
||||
// Backend already gates login behind the PIN check, so no separate
|
||||
// OTP possession-of-phone step is needed for returning users.
|
||||
setState(() => _loading = false);
|
||||
Navigator.pushNamed(context, Routes.enterPin);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
state.isLoginFlow = false;
|
||||
setState(() => _loading = false);
|
||||
SnackbarUtils.showError(context, e.toString());
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sms_autofill/sms_autofill.dart';
|
||||
|
||||
import 'package:doormile/main.dart';
|
||||
import 'package:doormile/shared/state/app_state.dart';
|
||||
@@ -18,7 +21,7 @@ class OtpScreen extends StatefulWidget {
|
||||
State<OtpScreen> createState() => _OtpScreenState();
|
||||
}
|
||||
|
||||
class _OtpScreenState extends State<OtpScreen> {
|
||||
class _OtpScreenState extends State<OtpScreen> with CodeAutoFill {
|
||||
final List<TextEditingController> _controllers =
|
||||
List.generate(6, (_) => TextEditingController());
|
||||
final List<FocusNode> _nodes = List.generate(6, (_) => FocusNode());
|
||||
@@ -30,9 +33,40 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTimer();
|
||||
listenForCode();
|
||||
_prefillDebugBypass();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _nodes[0].requestFocus());
|
||||
}
|
||||
|
||||
/// Debug-only tester shortcut, see [AppState.verifyOtp]. Compiled out of
|
||||
/// release builds via kDebugMode.
|
||||
Future<void> _prefillDebugBypass() async {
|
||||
if (!kDebugMode) return;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final provider = prefs.getInt('smsDefaultProvider') ?? 0;
|
||||
final passKey = prefs.getInt('smsPassKey');
|
||||
if (provider != 1 || passKey == null) return;
|
||||
if (!mounted || _controllers.any((c) => c.text.isNotEmpty)) return;
|
||||
|
||||
final passKeyStr = passKey.toString().padLeft(6, '0');
|
||||
for (int i = 0; i < 6; i++) {
|
||||
_controllers[i].text = passKeyStr[i];
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void codeUpdated() {
|
||||
if (!mounted) return;
|
||||
final digits = (code ?? '').replaceAll(RegExp(r'[^0-9]'), '');
|
||||
if (digits.length < 6) return;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
_controllers[i].text = digits[i];
|
||||
}
|
||||
setState(() {});
|
||||
_verify();
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_seconds = 28;
|
||||
_timer?.cancel();
|
||||
@@ -59,8 +93,6 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
final state = context.read<AppState>();
|
||||
if (state.isResettingPin) {
|
||||
Navigator.pushReplacementNamed(context, Routes.resetPin);
|
||||
} else if (state.isLoginFlow) {
|
||||
Navigator.pushReplacementNamed(context, Routes.enterPin);
|
||||
} else {
|
||||
Navigator.pushReplacementNamed(context, Routes.setPin);
|
||||
}
|
||||
@@ -115,6 +147,7 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
cancel();
|
||||
for (final c in _controllers) {
|
||||
c.dispose();
|
||||
}
|
||||
@@ -137,8 +170,10 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: AppCard(
|
||||
padding: const EdgeInsets.all(28),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: AppCard(
|
||||
padding: const EdgeInsets.all(28),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -147,10 +182,10 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
height: 88,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.primaryFixed, shape: BoxShape.circle),
|
||||
child: const Icon(Icons.mark_email_read_rounded, size: 40, color: AppColors.primary),
|
||||
child: const Icon(Icons.sms_rounded, size: 40, color: AppColors.primary),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text('Verify your email',
|
||||
Text('Verify your number',
|
||||
textAlign: TextAlign.center, style: AppTheme.headlineLg),
|
||||
const SizedBox(height: 8),
|
||||
Text.rich(
|
||||
@@ -159,7 +194,7 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
children: [
|
||||
const TextSpan(text: 'We sent a 6-digit code to '),
|
||||
TextSpan(
|
||||
text: context.read<AppState>().pendingEmail ?? 'your email',
|
||||
text: '+91 $phone',
|
||||
style: AppTheme.labelMd,
|
||||
),
|
||||
],
|
||||
@@ -168,12 +203,19 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(6, (i) => _OtpBox(
|
||||
controller: _controllers[i],
|
||||
node: _nodes[i],
|
||||
onChanged: (v) => _onChanged(i, v),
|
||||
)),
|
||||
children: List.generate(
|
||||
6,
|
||||
(i) => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||
child: _OtpBox(
|
||||
controller: _controllers[i],
|
||||
node: _nodes[i],
|
||||
onChanged: (v) => _onChanged(i, v),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
if (_seconds > 0)
|
||||
@@ -198,6 +240,7 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -213,7 +256,6 @@ class _OtpBox extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final filled = controller.text.isNotEmpty;
|
||||
return SizedBox(
|
||||
width: 46,
|
||||
height: 56,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
|
||||
@@ -130,6 +130,12 @@ class _BookingPickupScreenState extends State<BookingPickupScreen> {
|
||||
if (match != null) draft.pickupPincode = match.group(0)!;
|
||||
}
|
||||
|
||||
if (draft.pickupPincode.isEmpty) {
|
||||
_showValidationError('Missing Pickup Pincode',
|
||||
'We couldn\'t detect a pincode for your pickup address. Please use "Use GPS current location", pick a saved address, or include the 6-digit pincode in the address.');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _loading = true);
|
||||
await appState.fetchPriceEstimate();
|
||||
setState(() => _loading = false);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:doormile/main.dart';
|
||||
import 'package:doormile/shared/state/app_state.dart';
|
||||
import 'package:doormile/core/utils/snackbar_utils.dart';
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/core/widgets/common.dart';
|
||||
@@ -51,13 +52,20 @@ class _PaymentSheetState extends State<_PaymentSheet> {
|
||||
state.draft.paymentMethod = _methods[_selected].name;
|
||||
await Future.delayed(const Duration(milliseconds: 1400));
|
||||
if (!mounted) return;
|
||||
state.placeOrder();
|
||||
Navigator.pop(context); // close sheet
|
||||
Navigator.pushNamedAndRemoveUntil(
|
||||
context,
|
||||
Routes.bookingSuccess,
|
||||
(r) => r.settings.name == Routes.home,
|
||||
);
|
||||
try {
|
||||
await state.placeOrder();
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context); // close sheet
|
||||
Navigator.pushNamedAndRemoveUntil(
|
||||
context,
|
||||
Routes.bookingSuccess,
|
||||
(r) => r.settings.name == Routes.home,
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _processing = false);
|
||||
SnackbarUtils.showError(context, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -78,7 +78,6 @@ class AppState extends ChangeNotifier {
|
||||
String? pendingEmail;
|
||||
String? authToken;
|
||||
bool isResettingPin = false;
|
||||
bool isLoginFlow = false;
|
||||
String? _deviceToken;
|
||||
|
||||
AppState() {
|
||||
@@ -127,52 +126,103 @@ class AppState extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Sends a 6-digit OTP to [phoneNumber] via Firebase Phone Auth.
|
||||
Future<void> sendOtp(String phoneNumber, {String? firstName, String? email, bool resetPin = false, required Function onSuccess, required Function(String) onError}) async {
|
||||
final fullPhone = '+91${phoneNumber.replaceAll(' ', '')}';
|
||||
setPhone(phoneNumber);
|
||||
pendingFirstName = firstName;
|
||||
pendingEmail = email;
|
||||
pendingFirstName = firstName ?? pendingFirstName;
|
||||
pendingEmail = email ?? pendingEmail;
|
||||
isResettingPin = resetPin;
|
||||
|
||||
if (email == null || email.isEmpty) {
|
||||
onError('Email address is required for verification.');
|
||||
final rawPhone = phoneNumber.replaceAll(' ', '');
|
||||
if (rawPhone.length != 10) {
|
||||
onError('A valid mobile number is required for verification.');
|
||||
return;
|
||||
}
|
||||
final fullPhone = '+91$rawPhone';
|
||||
|
||||
if (kIsWeb) {
|
||||
debugPrint('[OTP Service] Web build: bypassing Firebase OTP (mocking success)');
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
verificationId = 'mock_verification_id_$fullPhone';
|
||||
onSuccess();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final res = await http.post(
|
||||
Uri.parse(ApiConstants.sendEmailOtp),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({"email": email, "phone": fullPhone}),
|
||||
debugPrint('[OTP Service] Initiating Firebase phone verification for $fullPhone');
|
||||
await FirebaseAuth.instance.verifyPhoneNumber(
|
||||
phoneNumber: fullPhone,
|
||||
verificationCompleted: (PhoneAuthCredential credential) async {
|
||||
debugPrint('[OTP Service] Verification completed automatically');
|
||||
try {
|
||||
await FirebaseAuth.instance.signInWithCredential(credential);
|
||||
} catch (e) {
|
||||
debugPrint('[OTP Service] Error signing in: $e');
|
||||
}
|
||||
},
|
||||
verificationFailed: (FirebaseAuthException e) {
|
||||
debugPrint('[OTP Service] Verification failed: ${e.code} - ${e.message}');
|
||||
onError(e.message ?? 'Failed to send verification code.');
|
||||
},
|
||||
codeSent: (String vId, int? resendToken) {
|
||||
debugPrint('[OTP Service] Code sent. Verification ID: $vId');
|
||||
verificationId = vId;
|
||||
onSuccess();
|
||||
},
|
||||
codeAutoRetrievalTimeout: (String vId) {
|
||||
debugPrint('[OTP Service] Auto retrieval timeout. Verification ID: $vId');
|
||||
verificationId = vId;
|
||||
},
|
||||
);
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
onSuccess();
|
||||
} else {
|
||||
final body = jsonDecode(res.body);
|
||||
onError(body['message'] ?? 'Failed to send verification code.');
|
||||
}
|
||||
} catch (e) {
|
||||
onError('Error: $e');
|
||||
debugPrint('[OTP Service] Exception during OTP request: $e');
|
||||
onError('Error sending verification code: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyOtp(String smsCode, {required Function onSuccess, required Function(String) onError}) async {
|
||||
try {
|
||||
final res = await http.post(
|
||||
Uri.parse(ApiConstants.verifyEmailOtp),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({"email": pendingEmail, "otp": smsCode}),
|
||||
);
|
||||
/// Debug-only tester shortcut: if `smsDefaultProvider`/`smsPassKey` are set
|
||||
/// in SharedPreferences, that fixed passkey is accepted instead of the real
|
||||
/// OTP. `kDebugMode` means this branch is compiled out of release builds
|
||||
/// entirely, so it can never ship as a bypass.
|
||||
Future<bool> _checkDebugBypass(String smsCode) async {
|
||||
if (!kDebugMode) return false;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final provider = prefs.getInt('smsDefaultProvider') ?? 0;
|
||||
final passKey = prefs.getInt('smsPassKey');
|
||||
return provider == 1 && passKey != null && smsCode == passKey.toString().padLeft(6, '0');
|
||||
}
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
Future<void> verifyOtp(String smsCode, {required Function onSuccess, required Function(String) onError}) async {
|
||||
if (await _checkDebugBypass(smsCode)) {
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
if (verificationId == null) {
|
||||
onError('Verification ID is missing. Please resend OTP.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (kIsWeb) {
|
||||
debugPrint('[OTP Service] Web build: bypassing Firebase OTP verification (mocking success)');
|
||||
if (smsCode.length == 6) {
|
||||
onSuccess();
|
||||
} else {
|
||||
final body = jsonDecode(res.body);
|
||||
onError(body['message'] ?? 'Invalid verification code.');
|
||||
onError('Invalid mock OTP code (must be 6 digits).');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final credential = PhoneAuthProvider.credential(
|
||||
verificationId: verificationId!,
|
||||
smsCode: smsCode,
|
||||
);
|
||||
await FirebaseAuth.instance.signInWithCredential(credential);
|
||||
onSuccess();
|
||||
} catch (e) {
|
||||
onError('Error: $e');
|
||||
onError('Invalid verification code.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +367,6 @@ class AppState extends ChangeNotifier {
|
||||
|
||||
void login() {
|
||||
isLoggedIn = true;
|
||||
isLoginFlow = false;
|
||||
orders.clear();
|
||||
savedAddresses.clear();
|
||||
_saveSession();
|
||||
@@ -333,7 +382,6 @@ class AppState extends ChangeNotifier {
|
||||
verificationId = null;
|
||||
pendingFirstName = null;
|
||||
pendingEmail = null;
|
||||
isLoginFlow = false;
|
||||
orders.clear();
|
||||
savedAddresses.clear();
|
||||
notifyListeners();
|
||||
@@ -517,6 +565,8 @@ class AppState extends ChangeNotifier {
|
||||
"deliverypincode": draft.dropPincode,
|
||||
"deliveryaddress": draft.dropAddress,
|
||||
"serviceoption": draft.service,
|
||||
"contactname": draft.contactName,
|
||||
"contactmobile": draft.contactMobile,
|
||||
"parcels": [
|
||||
{
|
||||
"itemcategory": backendCategory,
|
||||
|
||||
Reference in New Issue
Block a user