feat: standard error handling, web bug fixes, and mock data setup
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="doormile"
|
android:label="doormile"
|
||||||
@@ -29,6 +30,10 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- Google Maps API key — replace with your key from Google Cloud Console -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q" />
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import GoogleMaps
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
@@ -7,6 +8,8 @@ import UIKit
|
|||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
// Replace with your key from Google Cloud Console
|
||||||
|
GMSServices.provideAPIKey("YOUR_GOOGLE_MAPS_API_KEY")
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
100
lib/core/utils/snackbar_utils.dart
Normal file
100
lib/core/utils/snackbar_utils.dart
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:doormile/core/theme/app_colors.dart';
|
||||||
|
import 'package:doormile/core/theme/app_theme.dart';
|
||||||
|
|
||||||
|
class SnackbarUtils {
|
||||||
|
static void showError(BuildContext context, String rawError) {
|
||||||
|
String message = _getProfessionalErrorMessage(rawError);
|
||||||
|
|
||||||
|
final snackBar = SnackBar(
|
||||||
|
content: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error_outline_rounded, color: Colors.white, size: 24),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: AppTheme.bodyMd.copyWith(color: Colors.white, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: AppColors.error,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
elevation: 4,
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..hideCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void showSuccess(BuildContext context, String message) {
|
||||||
|
final snackBar = SnackBar(
|
||||||
|
content: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.check_circle_outline_rounded, color: Colors.white, size: 24),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: AppTheme.bodyMd.copyWith(color: Colors.white, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xFF4CAF50), // Green success color
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
elevation: 4,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..hideCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _getProfessionalErrorMessage(String raw) {
|
||||||
|
final lowerRaw = raw.toLowerCase();
|
||||||
|
|
||||||
|
// Check for network errors
|
||||||
|
if (lowerRaw.contains('socketexception') ||
|
||||||
|
lowerRaw.contains('connection refused') ||
|
||||||
|
lowerRaw.contains('failed host lookup') ||
|
||||||
|
lowerRaw.contains('network') ||
|
||||||
|
lowerRaw.contains('timeout')) {
|
||||||
|
return 'Please check your internet connection and try again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common auth and business errors
|
||||||
|
if (lowerRaw.contains('invalid otp') || lowerRaw.contains('incorrect otp')) {
|
||||||
|
return 'The OTP you entered is incorrect. Please try again.';
|
||||||
|
}
|
||||||
|
if (lowerRaw.contains('user not found') || lowerRaw.contains('not registered')) {
|
||||||
|
return 'Account not found. Please check your details.';
|
||||||
|
}
|
||||||
|
if (lowerRaw.contains('already registered') || lowerRaw.contains('exists')) {
|
||||||
|
return 'This phone number is already registered.';
|
||||||
|
}
|
||||||
|
if (lowerRaw.contains('pin')) {
|
||||||
|
return 'Incorrect PIN. Please try again.';
|
||||||
|
}
|
||||||
|
if (lowerRaw.contains('unauthorized') || lowerRaw.contains('expired')) {
|
||||||
|
return 'Your session has expired. Please log in again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default generic server / unknown error
|
||||||
|
return 'Something went wrong. Please try again later.';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:doormile/main.dart';
|
import 'package:doormile/main.dart';
|
||||||
import 'package:doormile/shared/state/app_state.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_colors.dart';
|
||||||
import 'package:doormile/core/theme/app_theme.dart';
|
import 'package:doormile/core/theme/app_theme.dart';
|
||||||
import 'package:doormile/core/widgets/common.dart';
|
import 'package:doormile/core/widgets/common.dart';
|
||||||
@@ -28,15 +29,39 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
|
|
||||||
|
final state = context.read<AppState>();
|
||||||
try {
|
try {
|
||||||
await context.read<AppState>().initiateLogin(_phone.text);
|
await state.initiateLogin(_phone.text);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _loading = false);
|
|
||||||
Navigator.pushNamed(context, Routes.enterPin);
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
state.isLoginFlow = false;
|
||||||
setState(() => _loading = false);
|
setState(() => _loading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))));
|
SnackbarUtils.showError(context, e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:doormile/main.dart';
|
import 'package:doormile/main.dart';
|
||||||
import 'package:doormile/shared/state/app_state.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_colors.dart';
|
||||||
import 'package:doormile/core/theme/app_theme.dart';
|
import 'package:doormile/core/theme/app_theme.dart';
|
||||||
import 'package:doormile/core/widgets/common.dart';
|
import 'package:doormile/core/widgets/common.dart';
|
||||||
@@ -59,6 +60,8 @@ class _OtpScreenState extends State<OtpScreen> {
|
|||||||
final state = context.read<AppState>();
|
final state = context.read<AppState>();
|
||||||
if (state.isResettingPin) {
|
if (state.isResettingPin) {
|
||||||
Navigator.pushReplacementNamed(context, Routes.resetPin);
|
Navigator.pushReplacementNamed(context, Routes.resetPin);
|
||||||
|
} else if (state.isLoginFlow) {
|
||||||
|
Navigator.pushReplacementNamed(context, Routes.enterPin);
|
||||||
} else {
|
} else {
|
||||||
Navigator.pushReplacementNamed(context, Routes.setPin);
|
Navigator.pushReplacementNamed(context, Routes.setPin);
|
||||||
}
|
}
|
||||||
@@ -66,7 +69,7 @@ class _OtpScreenState extends State<OtpScreen> {
|
|||||||
onError: (err) {
|
onError: (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _loading = false);
|
setState(() => _loading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err)));
|
SnackbarUtils.showError(context, err);
|
||||||
for (var c in _controllers) {
|
for (var c in _controllers) {
|
||||||
c.clear();
|
c.clear();
|
||||||
}
|
}
|
||||||
@@ -83,12 +86,12 @@ class _OtpScreenState extends State<OtpScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _loading = false);
|
setState(() => _loading = false);
|
||||||
_startTimer();
|
_startTimer();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('OTP Resent')));
|
SnackbarUtils.showSuccess(context, 'OTP Resent');
|
||||||
},
|
},
|
||||||
onError: (err) {
|
onError: (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _loading = false);
|
setState(() => _loading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err)));
|
SnackbarUtils.showError(context, err);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:doormile/main.dart';
|
import 'package:doormile/main.dart';
|
||||||
import 'package:doormile/shared/state/app_state.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_colors.dart';
|
||||||
import 'package:doormile/core/theme/app_theme.dart';
|
import 'package:doormile/core/theme/app_theme.dart';
|
||||||
import 'package:doormile/core/widgets/common.dart';
|
import 'package:doormile/core/widgets/common.dart';
|
||||||
@@ -81,7 +82,7 @@ class _PinScreenState extends State<PinScreen> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _loading = false);
|
setState(() => _loading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))));
|
SnackbarUtils.showError(context, e.toString());
|
||||||
for (var c in _controllers) c.clear();
|
for (var c in _controllers) c.clear();
|
||||||
_nodes[0].requestFocus();
|
_nodes[0].requestFocus();
|
||||||
_updateCode();
|
_updateCode();
|
||||||
@@ -101,7 +102,7 @@ class _PinScreenState extends State<PinScreen> {
|
|||||||
onError: (err) {
|
onError: (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _loading = false);
|
setState(() => _loading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err)));
|
SnackbarUtils.showError(context, err);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:doormile/main.dart';
|
import 'package:doormile/main.dart';
|
||||||
import 'package:doormile/shared/state/app_state.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_colors.dart';
|
||||||
import 'package:doormile/core/theme/app_theme.dart';
|
import 'package:doormile/core/theme/app_theme.dart';
|
||||||
import 'package:doormile/core/widgets/common.dart';
|
import 'package:doormile/core/widgets/common.dart';
|
||||||
@@ -44,7 +45,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
onError: (err) {
|
onError: (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _loading = false);
|
setState(() => _loading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err)));
|
SnackbarUtils.showError(context, err);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:geocoding/geocoding.dart';
|
|||||||
|
|
||||||
import 'package:doormile/main.dart';
|
import 'package:doormile/main.dart';
|
||||||
import 'package:doormile/shared/state/app_state.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_colors.dart';
|
||||||
import 'package:doormile/core/theme/app_theme.dart';
|
import 'package:doormile/core/theme/app_theme.dart';
|
||||||
import 'package:doormile/core/widgets/common.dart';
|
import 'package:doormile/core/widgets/common.dart';
|
||||||
@@ -123,6 +124,12 @@ class _BookingPickupScreenState extends State<BookingPickupScreen> {
|
|||||||
draft.parcelType = _category;
|
draft.parcelType = _category;
|
||||||
draft.service = _serviceType;
|
draft.service = _serviceType;
|
||||||
|
|
||||||
|
// Extract pincode from typed address if GPS/saved address didn't set it.
|
||||||
|
if (draft.pickupPincode.isEmpty) {
|
||||||
|
final match = RegExp(r'\b\d{6}\b').firstMatch(_pickup.text);
|
||||||
|
if (match != null) draft.pickupPincode = match.group(0)!;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
await appState.fetchPriceEstimate();
|
await appState.fetchPriceEstimate();
|
||||||
setState(() => _loading = false);
|
setState(() => _loading = false);
|
||||||
@@ -229,16 +236,14 @@ class _BookingPickupScreenState extends State<BookingPickupScreen> {
|
|||||||
final area = '${place.subLocality ?? place.locality}, ${place.administrativeArea}';
|
final area = '${place.subLocality ?? place.locality}, ${place.administrativeArea}';
|
||||||
final pincode = place.postalCode ?? '';
|
final pincode = place.postalCode ?? '';
|
||||||
final fullAddress = '${place.street}, $area, $pincode';
|
final fullAddress = '${place.street}, $area, $pincode';
|
||||||
_showLocationConfirmationSheet(context, state, area, pincode, fullAddress);
|
_showLocationConfirmationSheet(context, state, area, pincode, fullAddress, position.latitude, position.longitude);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Could not determine address from coordinates.');
|
throw Exception('Could not determine address from coordinates.');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _loading = false);
|
setState(() => _loading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
SnackbarUtils.showError(context, e.toString());
|
||||||
SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -268,9 +273,11 @@ class _BookingPickupScreenState extends State<BookingPickupScreen> {
|
|||||||
icon: Icons.home_rounded,
|
icon: Icons.home_rounded,
|
||||||
label: 'Home',
|
label: 'Home',
|
||||||
address: '12th Main, Indiranagar, Bengaluru, Karnataka 560038',
|
address: '12th Main, Indiranagar, Bengaluru, Karnataka 560038',
|
||||||
onSelect: (addr) => setState(() {
|
pincode: '560038',
|
||||||
|
onSelect: (addr, pin) => setState(() {
|
||||||
_pickup.text = addr;
|
_pickup.text = addr;
|
||||||
final state = context.read<AppState>();
|
final state = context.read<AppState>();
|
||||||
|
state.draft.pickupPincode = pin;
|
||||||
_name.text = state.firstName;
|
_name.text = state.firstName;
|
||||||
_mobile.text = state.phone;
|
_mobile.text = state.phone;
|
||||||
}),
|
}),
|
||||||
@@ -280,9 +287,11 @@ class _BookingPickupScreenState extends State<BookingPickupScreen> {
|
|||||||
icon: Icons.business_rounded,
|
icon: Icons.business_rounded,
|
||||||
label: 'Office',
|
label: 'Office',
|
||||||
address: 'Sector 4, HSR Layout, Bengaluru, Karnataka 560102',
|
address: 'Sector 4, HSR Layout, Bengaluru, Karnataka 560102',
|
||||||
onSelect: (addr) => setState(() {
|
pincode: '560102',
|
||||||
|
onSelect: (addr, pin) => setState(() {
|
||||||
_pickup.text = addr;
|
_pickup.text = addr;
|
||||||
final state = context.read<AppState>();
|
final state = context.read<AppState>();
|
||||||
|
state.draft.pickupPincode = pin;
|
||||||
_name.text = state.firstName;
|
_name.text = state.firstName;
|
||||||
_mobile.text = state.phone;
|
_mobile.text = state.phone;
|
||||||
}),
|
}),
|
||||||
@@ -292,9 +301,11 @@ class _BookingPickupScreenState extends State<BookingPickupScreen> {
|
|||||||
icon: Icons.favorite_rounded,
|
icon: Icons.favorite_rounded,
|
||||||
label: "Mom's House",
|
label: "Mom's House",
|
||||||
address: 'Jayanagar 4th Block, Bengaluru, Karnataka 560011',
|
address: 'Jayanagar 4th Block, Bengaluru, Karnataka 560011',
|
||||||
onSelect: (addr) => setState(() {
|
pincode: '560011',
|
||||||
|
onSelect: (addr, pin) => setState(() {
|
||||||
_pickup.text = addr;
|
_pickup.text = addr;
|
||||||
final state = context.read<AppState>();
|
final state = context.read<AppState>();
|
||||||
|
state.draft.pickupPincode = pin;
|
||||||
_name.text = state.firstName;
|
_name.text = state.firstName;
|
||||||
_mobile.text = state.phone;
|
_mobile.text = state.phone;
|
||||||
}),
|
}),
|
||||||
@@ -516,7 +527,13 @@ class _BookingPickupScreenState extends State<BookingPickupScreen> {
|
|||||||
...state.savedAddresses.map((a) => Padding(
|
...state.savedAddresses.map((a) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: AppCard(
|
child: AppCard(
|
||||||
onTap: () => setState(() => _pickup.text = '${a.label}, ${a.line}'),
|
onTap: () {
|
||||||
|
final draft = context.read<AppState>().draft;
|
||||||
|
draft.pickupPincode = a.pincode;
|
||||||
|
draft.pickupLatitude = a.latitude;
|
||||||
|
draft.pickupLongitude = a.longitude;
|
||||||
|
setState(() => _pickup.text = '${a.label}, ${a.line}');
|
||||||
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconBadge(
|
IconBadge(
|
||||||
@@ -543,7 +560,7 @@ class _BookingPickupScreenState extends State<BookingPickupScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showLocationConfirmationSheet(BuildContext context, AppState state, String area, String pincode, String fullAddress) {
|
void _showLocationConfirmationSheet(BuildContext context, AppState state, String area, String pincode, String fullAddress, double latitude, double longitude) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@@ -597,6 +614,9 @@ class _BookingPickupScreenState extends State<BookingPickupScreen> {
|
|||||||
child: PrimaryButton(
|
child: PrimaryButton(
|
||||||
label: 'Confirm',
|
label: 'Confirm',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
state.draft.pickupPincode = pincode;
|
||||||
|
state.draft.pickupLatitude = latitude;
|
||||||
|
state.draft.pickupLongitude = longitude;
|
||||||
setState(() {
|
setState(() {
|
||||||
_pickup.text = fullAddress;
|
_pickup.text = fullAddress;
|
||||||
_name.text = state.firstName;
|
_name.text = state.firstName;
|
||||||
@@ -679,19 +699,21 @@ class _SavedAddressChip extends StatelessWidget {
|
|||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final String address;
|
final String address;
|
||||||
final Function(String) onSelect;
|
final String pincode;
|
||||||
|
final Function(String address, String pincode) onSelect;
|
||||||
|
|
||||||
const _SavedAddressChip({
|
const _SavedAddressChip({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.address,
|
required this.address,
|
||||||
|
required this.pincode,
|
||||||
required this.onSelect,
|
required this.onSelect,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => onSelect(address),
|
onTap: () => onSelect(address, pincode),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:doormile/main.dart';
|
import 'package:doormile/main.dart';
|
||||||
import 'package:doormile/shared/state/app_state.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_colors.dart';
|
||||||
import 'package:doormile/core/theme/app_theme.dart';
|
import 'package:doormile/core/theme/app_theme.dart';
|
||||||
import 'package:doormile/core/widgets/common.dart';
|
import 'package:doormile/core/widgets/common.dart';
|
||||||
@@ -29,7 +30,7 @@ class _BookingPreviewScreenState extends State<BookingPreviewScreen> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _loading = false);
|
setState(() => _loading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))));
|
SnackbarUtils.showError(context, e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
import 'package:doormile/main.dart';
|
import 'package:doormile/main.dart';
|
||||||
@@ -28,9 +29,14 @@ class _MainShellState extends State<MainShell> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _requestPermissions() async {
|
Future<void> _requestPermissions() async {
|
||||||
final status = await Permission.locationWhenInUse.status;
|
if (kIsWeb) return;
|
||||||
if (!status.isGranted) {
|
try {
|
||||||
await Permission.locationWhenInUse.request();
|
final status = await Permission.locationWhenInUse.status;
|
||||||
|
if (!status.isGranted) {
|
||||||
|
await Permission.locationWhenInUse.request();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Location permission error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,30 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
import 'package:doormile/core/theme/app_colors.dart';
|
import 'package:doormile/core/theme/app_colors.dart';
|
||||||
import 'package:doormile/core/theme/app_theme.dart';
|
import 'package:doormile/core/theme/app_theme.dart';
|
||||||
import 'package:doormile/core/widgets/common.dart';
|
|
||||||
|
enum _ConnStatus { connecting, connected, disconnected }
|
||||||
|
|
||||||
|
class _ChatMessage {
|
||||||
|
final String text;
|
||||||
|
final bool fromCustomer;
|
||||||
|
_ChatMessage(this.text, {required this.fromCustomer});
|
||||||
|
}
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatefulWidget {
|
||||||
final String riderName;
|
final String riderName;
|
||||||
const ChatScreen({super.key, required this.riderName});
|
final String bookingId;
|
||||||
|
final String? authToken;
|
||||||
|
|
||||||
|
const ChatScreen({
|
||||||
|
super.key,
|
||||||
|
required this.riderName,
|
||||||
|
required this.bookingId,
|
||||||
|
this.authToken,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatScreen> createState() => _ChatScreenState();
|
State<ChatScreen> createState() => _ChatScreenState();
|
||||||
@@ -14,25 +32,117 @@ class ChatScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _ChatScreenState extends State<ChatScreen> {
|
class _ChatScreenState extends State<ChatScreen> {
|
||||||
final TextEditingController _messageController = TextEditingController();
|
final TextEditingController _messageController = TextEditingController();
|
||||||
final List<String> _messages = [
|
final ScrollController _scrollController = ScrollController();
|
||||||
'Hi! I am picking up your parcel now.',
|
final List<_ChatMessage> _messages = [];
|
||||||
'Please let me know if there are any specific instructions.',
|
|
||||||
];
|
WebSocketChannel? _channel;
|
||||||
|
_ConnStatus _status = _ConnStatus.connecting;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_connectWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _connectWebSocket() {
|
||||||
|
if (widget.bookingId.isEmpty) {
|
||||||
|
setState(() => _status = _ConnStatus.disconnected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _status = _ConnStatus.connecting);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String wsUrl = 'ws://api.doormile.com/ws/bookings/${widget.bookingId}/chat?role=customer';
|
||||||
|
if (widget.authToken != null && widget.authToken!.isNotEmpty) {
|
||||||
|
wsUrl += '&token=${widget.authToken}';
|
||||||
|
}
|
||||||
|
|
||||||
|
_channel = WebSocketChannel.connect(
|
||||||
|
Uri.parse(wsUrl),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger connected status on first successful frame after connect
|
||||||
|
_channel!.ready.then((_) {
|
||||||
|
if (mounted) setState(() => _status = _ConnStatus.connected);
|
||||||
|
}).catchError((e) {
|
||||||
|
debugPrint('WS ready error: $e');
|
||||||
|
if (mounted) setState(() => _status = _ConnStatus.disconnected);
|
||||||
|
});
|
||||||
|
|
||||||
|
_channel!.stream.listen(
|
||||||
|
_onMessage,
|
||||||
|
onError: (e) {
|
||||||
|
debugPrint('WS error: $e');
|
||||||
|
if (mounted) setState(() => _status = _ConnStatus.disconnected);
|
||||||
|
},
|
||||||
|
onDone: () {
|
||||||
|
if (mounted) setState(() => _status = _ConnStatus.disconnected);
|
||||||
|
},
|
||||||
|
cancelOnError: false,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('WS connect error: $e');
|
||||||
|
if (mounted) setState(() => _status = _ConnStatus.disconnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMessage(dynamic raw) {
|
||||||
|
try {
|
||||||
|
final data = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||||
|
final text = data['text'] as String? ?? '';
|
||||||
|
final sender = data['sender'] as String? ?? '';
|
||||||
|
if (text.isEmpty) return;
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_messages.add(_ChatMessage(text, fromCustomer: sender == 'customer'));
|
||||||
|
});
|
||||||
|
_scrollToBottom();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('WS parse error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendMessage() {
|
||||||
|
final text = _messageController.text.trim();
|
||||||
|
if (text.isEmpty) return;
|
||||||
|
|
||||||
|
final payload = jsonEncode({'text': text, 'sender': 'customer'});
|
||||||
|
|
||||||
|
if (_channel != null && _status == _ConnStatus.connected) {
|
||||||
|
_channel!.sink.add(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimistically show the message regardless of WS state
|
||||||
|
setState(() {
|
||||||
|
_messages.add(_ChatMessage(text, fromCustomer: true));
|
||||||
|
_messageController.clear();
|
||||||
|
});
|
||||||
|
_scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollToBottom() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (_scrollController.hasClients) {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
_scrollController.position.maxScrollExtent,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_messageController.dispose();
|
_messageController.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
_channel?.sink.close();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendMessage() {
|
|
||||||
if (_messageController.text.trim().isEmpty) return;
|
|
||||||
setState(() {
|
|
||||||
_messages.add(_messageController.text.trim());
|
|
||||||
_messageController.clear();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -43,43 +153,106 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(widget.riderName, style: AppTheme.headlineSm.copyWith(color: Colors.white, fontSize: 18)),
|
Text(widget.riderName,
|
||||||
Text('Doormile EV Rider', style: AppTheme.caption.copyWith(color: Colors.white70)),
|
style: AppTheme.headlineSm.copyWith(color: Colors.white, fontSize: 18)),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
margin: const EdgeInsets.only(right: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: switch (_status) {
|
||||||
|
_ConnStatus.connected => const Color(0xFF4CAF50),
|
||||||
|
_ConnStatus.connecting => const Color(0xFFFF9800),
|
||||||
|
_ConnStatus.disconnected => const Color(0xFFEF5350),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
switch (_status) {
|
||||||
|
_ConnStatus.connected => 'Connected',
|
||||||
|
_ConnStatus.connecting => 'Connecting...',
|
||||||
|
_ConnStatus.disconnected => 'Disconnected',
|
||||||
|
},
|
||||||
|
style: AppTheme.caption.copyWith(color: Colors.white70),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
actions: [
|
||||||
|
if (_status == _ConnStatus.disconnected)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh_rounded),
|
||||||
|
tooltip: 'Reconnect',
|
||||||
|
onPressed: () {
|
||||||
|
_channel?.sink.close();
|
||||||
|
_connectWebSocket();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: _messages.isEmpty
|
||||||
padding: const EdgeInsets.all(16),
|
? Center(
|
||||||
itemCount: _messages.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final isRider = index < 2; // Mocking first 2 messages from rider
|
|
||||||
return Align(
|
|
||||||
alignment: isRider ? Alignment.centerLeft : Alignment.centerRight,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
|
||||||
constraints: const BoxConstraints(maxWidth: 280),
|
|
||||||
padding: const EdgeInsets.all(14),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isRider ? AppColors.surfaceContainerHigh : AppColors.primary,
|
|
||||||
borderRadius: BorderRadius.circular(16).copyWith(
|
|
||||||
bottomLeft: isRider ? Radius.zero : const Radius.circular(16),
|
|
||||||
bottomRight: !isRider ? Radius.zero : const Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
_messages[index],
|
_status == _ConnStatus.connecting
|
||||||
style: AppTheme.bodyMd.copyWith(color: isRider ? AppColors.onSurface : Colors.white),
|
? 'Connecting to chat...'
|
||||||
|
: 'No messages yet. Say hi!',
|
||||||
|
style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: _messages.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final msg = _messages[index];
|
||||||
|
return Align(
|
||||||
|
alignment: msg.fromCustomer
|
||||||
|
? Alignment.centerRight
|
||||||
|
: Alignment.centerLeft,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
constraints: const BoxConstraints(maxWidth: 280),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: msg.fromCustomer
|
||||||
|
? AppColors.primary
|
||||||
|
: AppColors.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(16).copyWith(
|
||||||
|
bottomLeft: msg.fromCustomer
|
||||||
|
? const Radius.circular(16)
|
||||||
|
: Radius.zero,
|
||||||
|
bottomRight: msg.fromCustomer
|
||||||
|
? Radius.zero
|
||||||
|
: const Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
msg.text,
|
||||||
|
style: AppTheme.bodyMd.copyWith(
|
||||||
|
color: msg.fromCustomer
|
||||||
|
? Colors.white
|
||||||
|
: AppColors.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16).copyWith(bottom: MediaQuery.of(context).padding.bottom + 16),
|
padding: EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 16,
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
|
),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border(top: BorderSide(color: AppColors.surfaceContainer)),
|
border: Border(top: BorderSide(color: AppColors.surfaceContainer)),
|
||||||
@@ -89,6 +262,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _messageController,
|
controller: _messageController,
|
||||||
|
textInputAction: TextInputAction.send,
|
||||||
|
onSubmitted: (_) => _sendMessage(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Message ${widget.riderName}...',
|
hintText: 'Message ${widget.riderName}...',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@@ -97,7 +272,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.surfaceContainerLowest,
|
fillColor: AppColors.surfaceContainerLowest,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,28 +1,157 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
import 'package:doormile/shared/models/models.dart';
|
import 'package:doormile/shared/models/models.dart';
|
||||||
import 'package:doormile/core/theme/app_colors.dart';
|
import 'package:doormile/core/theme/app_colors.dart';
|
||||||
|
import 'package:doormile/core/utils/snackbar_utils.dart';
|
||||||
import 'package:doormile/core/theme/app_theme.dart';
|
import 'package:doormile/core/theme/app_theme.dart';
|
||||||
import 'package:doormile/core/widgets/common.dart';
|
import 'package:doormile/core/widgets/common.dart';
|
||||||
|
|
||||||
class LiveTrackingScreen extends StatelessWidget {
|
class LiveTrackingScreen extends StatefulWidget {
|
||||||
final Order? order;
|
final Order? order;
|
||||||
const LiveTrackingScreen({super.key, this.order});
|
const LiveTrackingScreen({super.key, this.order});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LiveTrackingScreen> createState() => _LiveTrackingScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LiveTrackingScreenState extends State<LiveTrackingScreen> {
|
||||||
|
static const _defaultCenter = LatLng(11.02, 76.95);
|
||||||
|
|
||||||
|
WebSocketChannel? _channel;
|
||||||
|
GoogleMapController? _mapController;
|
||||||
|
|
||||||
|
LatLng _milerPosition = _defaultCenter;
|
||||||
|
Set<Marker> _markers = {};
|
||||||
|
int _etaSeconds = 0;
|
||||||
|
String _milerName = 'Assigning Miler...';
|
||||||
|
String _status = 'In transit';
|
||||||
|
Timer? _etaTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_connectWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _connectWebSocket() {
|
||||||
|
final bookingId = widget.order?.id;
|
||||||
|
if (bookingId == null || bookingId.isEmpty) return;
|
||||||
|
try {
|
||||||
|
_channel = WebSocketChannel.connect(
|
||||||
|
Uri.parse('ws://api.doormile.com/ws/bookings/$bookingId/track'),
|
||||||
|
);
|
||||||
|
_channel!.stream.listen(
|
||||||
|
_onMessage,
|
||||||
|
onError: (e) => debugPrint('WS error: $e'),
|
||||||
|
onDone: () => debugPrint('WS closed'),
|
||||||
|
cancelOnError: false,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('WS connect error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMessage(dynamic raw) {
|
||||||
|
final Map<String, dynamic> data;
|
||||||
|
try {
|
||||||
|
data = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('WS parse error: $e');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lat = (data['lat'] as num?)?.toDouble();
|
||||||
|
final lon = (data['lon'] as num?)?.toDouble();
|
||||||
|
final etaMinutes = (data['eta_minutes'] as num?)?.toInt();
|
||||||
|
final milerName = data['miler_name'] as String?;
|
||||||
|
final status = data['status'] as String?;
|
||||||
|
bool positionChanged = false;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (milerName != null && milerName.isNotEmpty) {
|
||||||
|
_milerName = milerName;
|
||||||
|
}
|
||||||
|
if (status != null && status.isNotEmpty) {
|
||||||
|
_status = status;
|
||||||
|
}
|
||||||
|
if (lat != null && lon != null && (lat != 0.0 || lon != 0.0)) {
|
||||||
|
_milerPosition = LatLng(lat, lon);
|
||||||
|
positionChanged = true;
|
||||||
|
_markers = {
|
||||||
|
Marker(
|
||||||
|
markerId: const MarkerId('miler'),
|
||||||
|
position: _milerPosition,
|
||||||
|
infoWindow: InfoWindow(title: _milerName),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (etaMinutes != null) {
|
||||||
|
_etaSeconds = etaMinutes * 60;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (etaMinutes != null) _restartEtaTimer();
|
||||||
|
|
||||||
|
if (positionChanged) {
|
||||||
|
_mapController?.animateCamera(
|
||||||
|
CameraUpdate.newLatLng(_milerPosition),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _restartEtaTimer() {
|
||||||
|
_etaTimer?.cancel();
|
||||||
|
if (_etaSeconds <= 0) return;
|
||||||
|
_etaTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
if (_etaSeconds > 0) _etaSeconds--;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _etaLabel {
|
||||||
|
if (_etaSeconds > 0) {
|
||||||
|
return '${(_etaSeconds / 60).ceil()} min';
|
||||||
|
}
|
||||||
|
return widget.order?.eta ?? '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_etaTimer?.cancel();
|
||||||
|
_channel?.sink.close();
|
||||||
|
_mapController?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final id = order?.id ?? 'DM-CHN-BLR-7741';
|
final id = widget.order?.id ?? 'DM-CHN-BLR-7741';
|
||||||
final eta = order?.eta ?? '2:30 PM';
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.surface,
|
backgroundColor: AppColors.surface,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Faux map
|
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: CustomPaint(painter: _MapPainter()),
|
child: GoogleMap(
|
||||||
|
initialCameraPosition: const CameraPosition(
|
||||||
|
target: _defaultCenter,
|
||||||
|
zoom: 14,
|
||||||
|
),
|
||||||
|
markers: _markers,
|
||||||
|
onMapCreated: (controller) => _mapController = controller,
|
||||||
|
myLocationButtonEnabled: false,
|
||||||
|
zoomControlsEnabled: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
// Top bar
|
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
@@ -46,7 +175,6 @@ class LiveTrackingScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Bottom detail sheet
|
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -81,14 +209,14 @@ class LiveTrackingScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
StatusChip(
|
StatusChip(
|
||||||
label: 'In transit',
|
label: _status,
|
||||||
color: AppColors.tertiary,
|
color: AppColors.tertiary,
|
||||||
bg: AppColors.tertiaryFixed.withValues(alpha: 0.5),
|
bg: AppColors.tertiaryFixed.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text('ETA $eta',
|
Text('ETA $_etaLabel',
|
||||||
style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
|
style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const _MileStepper(activeIndex: 2),
|
const _MileStepper(activeIndex: 2),
|
||||||
@@ -139,7 +267,7 @@ class LiveTrackingScreen extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('Rahul Sharma',
|
Text(_milerName,
|
||||||
style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)),
|
style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -195,7 +323,14 @@ class LiveTrackingScreen extends StatelessWidget {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () => Navigator.pushNamed(context, '/chat', arguments: 'Rahul Sharma'),
|
onPressed: () => Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
'/chat',
|
||||||
|
arguments: {
|
||||||
|
'milerName': _milerName,
|
||||||
|
'bookingId': widget.order?.id ?? '',
|
||||||
|
},
|
||||||
|
),
|
||||||
icon: const Icon(Icons.chat_bubble_outline, size: 18),
|
icon: const Icon(Icons.chat_bubble_outline, size: 18),
|
||||||
label: const Text('Chat'),
|
label: const Text('Chat'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@@ -216,13 +351,7 @@ class LiveTrackingScreen extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _toast(BuildContext context, String msg) {
|
void _toast(BuildContext context, String msg) {
|
||||||
ScaffoldMessenger.of(context)
|
SnackbarUtils.showError(context, msg);
|
||||||
..hideCurrentSnackBar()
|
|
||||||
..showSnackBar(SnackBar(
|
|
||||||
content: Text(msg),
|
|
||||||
backgroundColor: AppColors.inverseSurface,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +403,9 @@ class _MileStepper extends StatelessWidget {
|
|||||||
height: 3,
|
height: 3,
|
||||||
color: i == _stops.length - 1
|
color: i == _stops.length - 1
|
||||||
? Colors.transparent
|
? Colors.transparent
|
||||||
: (i < activeIndex ? _stops[i + 1].$2 : AppColors.surfaceContainerHigh),
|
: (i < activeIndex
|
||||||
|
? _stops[i + 1].$2
|
||||||
|
: AppColors.surfaceContainerHigh),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -292,71 +423,3 @@ class _MileStepper extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lightweight stylised street map with a dashed delivery route.
|
|
||||||
class _MapPainter extends CustomPainter {
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final bg = Paint()..color = const Color(0xFFEDEAE9);
|
|
||||||
canvas.drawRect(Offset.zero & size, bg);
|
|
||||||
|
|
||||||
final road = Paint()
|
|
||||||
..color = Colors.white
|
|
||||||
..strokeWidth = 10;
|
|
||||||
final thin = Paint()
|
|
||||||
..color = const Color(0xFFE0DCDB)
|
|
||||||
..strokeWidth = 4;
|
|
||||||
|
|
||||||
for (double x = 30; x < size.width; x += 70) {
|
|
||||||
canvas.drawLine(Offset(x, 0), Offset(x, size.height), thin);
|
|
||||||
}
|
|
||||||
for (double y = 80; y < size.height; y += 70) {
|
|
||||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), thin);
|
|
||||||
}
|
|
||||||
canvas.drawLine(Offset(0, size.height * 0.35),
|
|
||||||
Offset(size.width, size.height * 0.42), road);
|
|
||||||
canvas.drawLine(Offset(size.width * 0.4, 0),
|
|
||||||
Offset(size.width * 0.5, size.height * 0.7), road);
|
|
||||||
|
|
||||||
// Dashed route
|
|
||||||
final routePaint = Paint()
|
|
||||||
..color = AppColors.primary
|
|
||||||
..strokeWidth = 4
|
|
||||||
..style = PaintingStyle.stroke;
|
|
||||||
final path = Path()
|
|
||||||
..moveTo(size.width * 0.2, size.height * 0.6)
|
|
||||||
..quadraticBezierTo(size.width * 0.35, size.height * 0.3,
|
|
||||||
size.width * 0.55, size.height * 0.35)
|
|
||||||
..quadraticBezierTo(size.width * 0.75, size.height * 0.4,
|
|
||||||
size.width * 0.7, size.height * 0.18);
|
|
||||||
_drawDashed(canvas, path, routePaint);
|
|
||||||
|
|
||||||
// Origin & destination markers
|
|
||||||
final dot = Paint()..color = AppColors.primary;
|
|
||||||
canvas.drawCircle(Offset(size.width * 0.2, size.height * 0.6), 7, dot);
|
|
||||||
canvas.drawCircle(Offset(size.width * 0.2, size.height * 0.6), 3,
|
|
||||||
Paint()..color = Colors.white);
|
|
||||||
canvas.drawCircle(Offset(size.width * 0.7, size.height * 0.18), 9,
|
|
||||||
Paint()..color = AppColors.primary);
|
|
||||||
// Vehicle marker (mid route)
|
|
||||||
final mid = Offset(size.width * 0.55, size.height * 0.35);
|
|
||||||
canvas.drawCircle(mid, 16, Paint()..color = AppColors.primary.withValues(alpha: 0.2));
|
|
||||||
canvas.drawCircle(mid, 9, Paint()..color = AppColors.tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _drawDashed(Canvas canvas, Path path, Paint paint) {
|
|
||||||
const dash = 8.0;
|
|
||||||
const gap = 6.0;
|
|
||||||
for (final metric in path.computeMetrics()) {
|
|
||||||
double dist = 0;
|
|
||||||
while (dist < metric.length) {
|
|
||||||
canvas.drawPath(
|
|
||||||
metric.extractPath(dist, dist + dash), paint);
|
|
||||||
dist += dash + gap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -144,7 +144,17 @@ class DoormileApp extends StatelessWidget {
|
|||||||
page = const SavedAddressesScreen();
|
page = const SavedAddressesScreen();
|
||||||
break;
|
break;
|
||||||
case Routes.chat:
|
case Routes.chat:
|
||||||
page = ChatScreen(riderName: (settings.arguments as String?) ?? 'Rahul Sharma');
|
final chatArgs = settings.arguments as Map<String, String?>? ?? {};
|
||||||
|
page = Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final appState = Provider.of<AppState>(context, listen: false);
|
||||||
|
return ChatScreen(
|
||||||
|
riderName: chatArgs['milerName'] ?? 'Rider',
|
||||||
|
bookingId: chatArgs['bookingId'] ?? '',
|
||||||
|
authToken: appState.authToken,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
page = const MainShell();
|
page = const MainShell();
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ class AddressEntry {
|
|||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Color iconBg;
|
final Color iconBg;
|
||||||
final Color iconColor;
|
final Color iconColor;
|
||||||
|
final double latitude;
|
||||||
|
final double longitude;
|
||||||
|
final String pincode;
|
||||||
|
|
||||||
const AddressEntry({
|
const AddressEntry({
|
||||||
required this.label,
|
required this.label,
|
||||||
@@ -16,6 +19,9 @@ class AddressEntry {
|
|||||||
required this.icon,
|
required this.icon,
|
||||||
this.iconBg = AppColors.surfaceContainer,
|
this.iconBg = AppColors.surfaceContainer,
|
||||||
this.iconColor = AppColors.onSurfaceVariant,
|
this.iconColor = AppColors.onSurfaceVariant,
|
||||||
|
this.latitude = 0.0,
|
||||||
|
this.longitude = 0.0,
|
||||||
|
this.pincode = '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
@@ -14,6 +15,9 @@ import 'package:doormile/core/theme/app_colors.dart';
|
|||||||
/// Holds the in-progress booking the user is composing across the flow screens.
|
/// Holds the in-progress booking the user is composing across the flow screens.
|
||||||
class BookingDraft {
|
class BookingDraft {
|
||||||
String pickupAddress = '';
|
String pickupAddress = '';
|
||||||
|
String pickupPincode = '';
|
||||||
|
double pickupLatitude = 0.0;
|
||||||
|
double pickupLongitude = 0.0;
|
||||||
String contactName = '';
|
String contactName = '';
|
||||||
String contactMobile = '';
|
String contactMobile = '';
|
||||||
String notes = '';
|
String notes = '';
|
||||||
@@ -37,6 +41,9 @@ class BookingDraft {
|
|||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
pickupAddress = '';
|
pickupAddress = '';
|
||||||
|
pickupPincode = '';
|
||||||
|
pickupLatitude = 0.0;
|
||||||
|
pickupLongitude = 0.0;
|
||||||
contactName = '';
|
contactName = '';
|
||||||
contactMobile = '';
|
contactMobile = '';
|
||||||
notes = '';
|
notes = '';
|
||||||
@@ -71,6 +78,8 @@ class AppState extends ChangeNotifier {
|
|||||||
String? pendingEmail;
|
String? pendingEmail;
|
||||||
String? authToken;
|
String? authToken;
|
||||||
bool isResettingPin = false;
|
bool isResettingPin = false;
|
||||||
|
bool isLoginFlow = false;
|
||||||
|
String? _deviceToken;
|
||||||
|
|
||||||
AppState() {
|
AppState() {
|
||||||
_loadSession();
|
_loadSession();
|
||||||
@@ -101,6 +110,18 @@ class AppState extends ChangeNotifier {
|
|||||||
await prefs.setString('phone', phone);
|
await prefs.setString('phone', phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> _fetchDeviceToken() async {
|
||||||
|
try {
|
||||||
|
if (kIsWeb) return null;
|
||||||
|
await FirebaseMessaging.instance.requestPermission(alert: true, badge: true, sound: true);
|
||||||
|
_deviceToken = await FirebaseMessaging.instance.getToken();
|
||||||
|
return _deviceToken;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('FCM token error: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void setPhone(String value) {
|
void setPhone(String value) {
|
||||||
phone = value;
|
phone = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -213,6 +234,8 @@ class AppState extends ChangeNotifier {
|
|||||||
debugPrint('Error getting location for registration: $e');
|
debugPrint('Error getting location for registration: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final token = await _fetchDeviceToken();
|
||||||
|
|
||||||
final res = await http.post(
|
final res = await http.post(
|
||||||
Uri.parse(ApiConstants.register),
|
Uri.parse(ApiConstants.register),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
@@ -224,7 +247,8 @@ class AppState extends ChangeNotifier {
|
|||||||
"pin": pin,
|
"pin": pin,
|
||||||
"configid": 1001,
|
"configid": 1001,
|
||||||
"defaultlatitude": lat,
|
"defaultlatitude": lat,
|
||||||
"defaultlongitude": lng
|
"defaultlongitude": lng,
|
||||||
|
if (token != null) "device_token": token,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
@@ -277,10 +301,17 @@ class AppState extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final token = await _fetchDeviceToken();
|
||||||
|
|
||||||
final verifyRes = await http.post(
|
final verifyRes = await http.post(
|
||||||
Uri.parse(ApiConstants.verifyPin),
|
Uri.parse(ApiConstants.verifyPin),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: jsonEncode({"phone": rawPhone, "pin": pin, "configid": 1001}),
|
body: jsonEncode({
|
||||||
|
"phone": rawPhone,
|
||||||
|
"pin": pin,
|
||||||
|
"configid": 1001,
|
||||||
|
if (token != null) "device_token": token,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (verifyRes.statusCode >= 200 && verifyRes.statusCode < 300) {
|
if (verifyRes.statusCode >= 200 && verifyRes.statusCode < 300) {
|
||||||
@@ -316,7 +347,7 @@ class AppState extends ChangeNotifier {
|
|||||||
|
|
||||||
void login() {
|
void login() {
|
||||||
isLoggedIn = true;
|
isLoggedIn = true;
|
||||||
// Wipe hardcoded mock data so real users only see their own actual data!
|
isLoginFlow = false;
|
||||||
orders.clear();
|
orders.clear();
|
||||||
savedAddresses.clear();
|
savedAddresses.clear();
|
||||||
_saveSession();
|
_saveSession();
|
||||||
@@ -326,13 +357,13 @@ class AppState extends ChangeNotifier {
|
|||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
isLoggedIn = false;
|
isLoggedIn = false;
|
||||||
authToken = null;
|
authToken = null;
|
||||||
// Reset in-memory user details so the previous account does not linger.
|
|
||||||
firstName = 'User';
|
firstName = 'User';
|
||||||
userName = 'User';
|
userName = 'User';
|
||||||
phone = '';
|
phone = '';
|
||||||
verificationId = null;
|
verificationId = null;
|
||||||
pendingFirstName = null;
|
pendingFirstName = null;
|
||||||
pendingEmail = null;
|
pendingEmail = null;
|
||||||
|
isLoginFlow = false;
|
||||||
orders.clear();
|
orders.clear();
|
||||||
savedAddresses.clear();
|
savedAddresses.clear();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -360,6 +391,27 @@ class AppState extends ChangeNotifier {
|
|||||||
isLoadingOrders = true;
|
isLoadingOrders = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
if (authToken == 'easter-egg-token') {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
orders = [
|
||||||
|
Order(
|
||||||
|
id: 'DM-TEST-001',
|
||||||
|
fromLabel: 'Home (Mock)',
|
||||||
|
fromSub: 'Pickup',
|
||||||
|
toLabel: 'Pincode: 110001',
|
||||||
|
toSub: 'Delivery',
|
||||||
|
date: 'Today',
|
||||||
|
amount: 65.0,
|
||||||
|
status: OrderStatus.active,
|
||||||
|
progress: 0.1,
|
||||||
|
eta: '10 min',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
isLoadingOrders = false;
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('\n=== DOORMILE API FETCH ORDERS ===');
|
print('\n=== DOORMILE API FETCH ORDERS ===');
|
||||||
print('Endpoint: ${ApiConstants.getPickups}');
|
print('Endpoint: ${ApiConstants.getPickups}');
|
||||||
@@ -425,6 +477,7 @@ class AppState extends ChangeNotifier {
|
|||||||
final payload = {
|
final payload = {
|
||||||
"zone": "Local",
|
"zone": "Local",
|
||||||
"destinationpin": draft.dropPincode,
|
"destinationpin": draft.dropPincode,
|
||||||
|
"pickuppincode": draft.pickupPincode,
|
||||||
"service_type": draft.service.isNotEmpty ? draft.service : "Normal",
|
"service_type": draft.service.isNotEmpty ? draft.service : "Normal",
|
||||||
"weight": 1.0, // Default since we removed it from UI
|
"weight": 1.0, // Default since we removed it from UI
|
||||||
"category": backendCategory
|
"category": backendCategory
|
||||||
@@ -435,6 +488,14 @@ class AppState extends ChangeNotifier {
|
|||||||
print('Payload: ${jsonEncode(payload)}');
|
print('Payload: ${jsonEncode(payload)}');
|
||||||
print('====================================\n');
|
print('====================================\n');
|
||||||
|
|
||||||
|
if (authToken == 'easter-egg-token') {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
draft.minPrice = 50.0;
|
||||||
|
draft.maxPrice = 120.0;
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final res = await http.post(
|
final res = await http.post(
|
||||||
Uri.parse(ApiConstants.checkPrice),
|
Uri.parse(ApiConstants.checkPrice),
|
||||||
@@ -480,8 +541,12 @@ class AppState extends ChangeNotifier {
|
|||||||
|
|
||||||
final payload = {
|
final payload = {
|
||||||
"pickupaddress": draft.pickupAddress.isNotEmpty ? draft.pickupAddress : "Current Location (GPS)",
|
"pickupaddress": draft.pickupAddress.isNotEmpty ? draft.pickupAddress : "Current Location (GPS)",
|
||||||
"pickuppincode": "560001", // Default pincode until we parse from GPS
|
"pickuppincode": draft.pickupPincode,
|
||||||
|
"pickuplatitude": draft.pickupLatitude,
|
||||||
|
"pickuplongitude": draft.pickupLongitude,
|
||||||
"deliverypincode": draft.dropPincode,
|
"deliverypincode": draft.dropPincode,
|
||||||
|
"deliveryaddress": draft.dropAddress,
|
||||||
|
"serviceoption": draft.service,
|
||||||
"parcels": [
|
"parcels": [
|
||||||
{
|
{
|
||||||
"itemcategory": backendCategory,
|
"itemcategory": backendCategory,
|
||||||
@@ -495,6 +560,27 @@ class AppState extends ChangeNotifier {
|
|||||||
print('Payload: ${jsonEncode(payload)}');
|
print('Payload: ${jsonEncode(payload)}');
|
||||||
print('====================================\n');
|
print('====================================\n');
|
||||||
|
|
||||||
|
if (authToken == 'easter-egg-token') {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
final newOrder = Order(
|
||||||
|
id: 'DM-PU-000${101 + orders.length}',
|
||||||
|
fromLabel: draft.pickupAddress,
|
||||||
|
fromSub: draft.contactName.isEmpty ? 'Pickup requested' : 'Contact: ${draft.contactName}',
|
||||||
|
toLabel: draft.dropPincode.isNotEmpty ? 'Pincode: ${draft.dropPincode}' : 'Awaiting Miler input',
|
||||||
|
toSub: 'Drop details pending',
|
||||||
|
date: 'Today',
|
||||||
|
amount: draft.minPrice,
|
||||||
|
status: OrderStatus.active,
|
||||||
|
progress: 0.1,
|
||||||
|
eta: 'Assigning Miler...',
|
||||||
|
);
|
||||||
|
|
||||||
|
orders.insert(0, newOrder);
|
||||||
|
draft.reset();
|
||||||
|
notifyListeners();
|
||||||
|
return newOrder;
|
||||||
|
}
|
||||||
|
|
||||||
final res = await http.post(
|
final res = await http.post(
|
||||||
Uri.parse(ApiConstants.createPickup),
|
Uri.parse(ApiConstants.createPickup),
|
||||||
headers: {
|
headers: {
|
||||||
@@ -542,6 +628,7 @@ class AppState extends ChangeNotifier {
|
|||||||
icon: Icons.home_rounded,
|
icon: Icons.home_rounded,
|
||||||
iconBg: AppColors.primaryFixed,
|
iconBg: AppColors.primaryFixed,
|
||||||
iconColor: AppColors.primary,
|
iconColor: AppColors.primary,
|
||||||
|
pincode: '122003',
|
||||||
),
|
),
|
||||||
AddressEntry(
|
AddressEntry(
|
||||||
label: 'Work',
|
label: 'Work',
|
||||||
@@ -549,6 +636,7 @@ class AppState extends ChangeNotifier {
|
|||||||
icon: Icons.work_rounded,
|
icon: Icons.work_rounded,
|
||||||
iconBg: AppColors.tertiaryFixed,
|
iconBg: AppColors.tertiaryFixed,
|
||||||
iconColor: AppColors.tertiary,
|
iconColor: AppColors.tertiary,
|
||||||
|
pincode: '122002',
|
||||||
),
|
),
|
||||||
AddressEntry(
|
AddressEntry(
|
||||||
label: 'Select City Walk Mall',
|
label: 'Select City Walk Mall',
|
||||||
@@ -556,6 +644,7 @@ class AppState extends ChangeNotifier {
|
|||||||
icon: Icons.history_rounded,
|
icon: Icons.history_rounded,
|
||||||
iconBg: AppColors.surfaceContainerHigh,
|
iconBg: AppColors.surfaceContainerHigh,
|
||||||
iconColor: AppColors.onSurfaceVariant,
|
iconColor: AppColors.onSurfaceVariant,
|
||||||
|
pincode: '110017',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Foundation
|
|||||||
|
|
||||||
import firebase_auth
|
import firebase_auth
|
||||||
import firebase_core
|
import firebase_core
|
||||||
|
import firebase_messaging
|
||||||
import geolocator_apple
|
import geolocator_apple
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
@@ -15,6 +16,7 @@ import url_launcher_macos
|
|||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
|
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
|||||||
112
pubspec.lock
112
pubspec.lock
@@ -201,6 +201,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.8.0"
|
version: "3.8.0"
|
||||||
|
firebase_messaging:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_messaging
|
||||||
|
sha256: "9651e833454156b9f0927eaedccffba0f7f6a6e20ceddef82517211e7017e9c4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "16.3.0"
|
||||||
|
firebase_messaging_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_messaging_platform_interface
|
||||||
|
sha256: "292bb5dc9c4a429078895406c347d7c7690deb858c1adeed8f4b4346f769dfa3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.8.0"
|
||||||
|
firebase_messaging_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_messaging_web
|
||||||
|
sha256: "08c565bc83729439f5de2dfea77b4832002b705eb2f840366cefa80e4e5c3c66"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.0"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -238,6 +262,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.7"
|
version: "2.4.7"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.35"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -344,6 +376,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.5"
|
version: "0.2.5"
|
||||||
|
google_maps:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps
|
||||||
|
sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.2.0"
|
||||||
|
google_maps_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter
|
||||||
|
sha256: f4040715c2998144a46cfc0fd91ee83226655af069dfed3d2b84552dc5e4facb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.17.1"
|
||||||
|
google_maps_flutter_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter_android
|
||||||
|
sha256: "06c9c886e887dd61b9a8728c43344e58071279182bbc7a3c4001d322fc3cd111"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.19.10"
|
||||||
|
google_maps_flutter_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter_ios
|
||||||
|
sha256: f573749a75c4074b7a74ab986703607e9cd859fb2807c332872620f3656400dc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.18.3"
|
||||||
|
google_maps_flutter_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter_platform_interface
|
||||||
|
sha256: ddbe34435dfb34e83fca295c6a8dcc53c3b51487e9eec3c737ce4ae605574347
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.15.0"
|
||||||
|
google_maps_flutter_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter_web
|
||||||
|
sha256: e23f2869449bb3e2ed7cfdb178d716e1da7edc684363987e4cb2c42f2bd7a9e0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.2+3"
|
||||||
gsettings:
|
gsettings:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -592,6 +672,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5+1"
|
version: "6.1.5+1"
|
||||||
|
sanitize_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sanitize_html
|
||||||
|
sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -677,6 +765,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
stream_transform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_transform
|
||||||
|
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -813,6 +909,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket
|
||||||
|
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ dependencies:
|
|||||||
geolocator: ^14.0.2
|
geolocator: ^14.0.2
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
geocoding: ^4.0.0
|
geocoding: ^4.0.0
|
||||||
|
google_maps_flutter: ^2.9.0
|
||||||
|
web_socket_channel: ^3.0.1
|
||||||
|
firebase_messaging: 16.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user