diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6d17223..c5764b5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + + + Bool { + // Replace with your key from Google Cloud Console + GMSServices.provideAPIKey("YOUR_GOOGLE_MAPS_API_KEY") return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/lib/core/utils/snackbar_utils.dart b/lib/core/utils/snackbar_utils.dart new file mode 100644 index 0000000..b845c05 --- /dev/null +++ b/lib/core/utils/snackbar_utils.dart @@ -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.'; + } +} diff --git a/lib/features/auth/screens/login_screen.dart b/lib/features/auth/screens/login_screen.dart index d1d24bf..8a7205c 100644 --- a/lib/features/auth/screens/login_screen.dart +++ b/lib/features/auth/screens/login_screen.dart @@ -5,6 +5,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'; @@ -27,16 +28,40 @@ class _LoginScreenState extends State { if (!_valid) return; FocusScope.of(context).unfocus(); setState(() => _loading = true); - + + final state = context.read(); try { - await context.read().initiateLogin(_phone.text); + await state.initiateLogin(_phone.text); 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) { if (!mounted) return; + state.isLoginFlow = false; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', '')))); + SnackbarUtils.showError(context, e.toString()); } } diff --git a/lib/features/auth/screens/otp_screen.dart b/lib/features/auth/screens/otp_screen.dart index 0494a27..badf76a 100644 --- a/lib/features/auth/screens/otp_screen.dart +++ b/lib/features/auth/screens/otp_screen.dart @@ -6,6 +6,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'; @@ -59,6 +60,8 @@ class _OtpScreenState extends State { final state = context.read(); if (state.isResettingPin) { Navigator.pushReplacementNamed(context, Routes.resetPin); + } else if (state.isLoginFlow) { + Navigator.pushReplacementNamed(context, Routes.enterPin); } else { Navigator.pushReplacementNamed(context, Routes.setPin); } @@ -66,7 +69,7 @@ class _OtpScreenState extends State { onError: (err) { if (!mounted) return; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err))); + SnackbarUtils.showError(context, err); for (var c in _controllers) { c.clear(); } @@ -83,12 +86,12 @@ class _OtpScreenState extends State { if (!mounted) return; setState(() => _loading = false); _startTimer(); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('OTP Resent'))); + SnackbarUtils.showSuccess(context, 'OTP Resent'); }, onError: (err) { if (!mounted) return; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err))); + SnackbarUtils.showError(context, err); }, ); } diff --git a/lib/features/auth/screens/pin_screen.dart b/lib/features/auth/screens/pin_screen.dart index 96d6b2f..30589f6 100644 --- a/lib/features/auth/screens/pin_screen.dart +++ b/lib/features/auth/screens/pin_screen.dart @@ -4,6 +4,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'; @@ -81,7 +82,7 @@ class _PinScreenState extends State { } catch (e) { if (!mounted) return; 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(); _nodes[0].requestFocus(); _updateCode(); @@ -101,7 +102,7 @@ class _PinScreenState extends State { onError: (err) { if (!mounted) return; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err))); + SnackbarUtils.showError(context, err); }, ); } diff --git a/lib/features/auth/screens/signup_screen.dart b/lib/features/auth/screens/signup_screen.dart index aa0121f..eae38eb 100644 --- a/lib/features/auth/screens/signup_screen.dart +++ b/lib/features/auth/screens/signup_screen.dart @@ -4,6 +4,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'; @@ -44,7 +45,7 @@ class _SignupScreenState extends State { onError: (err) { if (!mounted) return; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err))); + SnackbarUtils.showError(context, err); }, ); } diff --git a/lib/features/booking/screens/booking_pickup_screen.dart b/lib/features/booking/screens/booking_pickup_screen.dart index aba40b0..9a11c69 100644 --- a/lib/features/booking/screens/booking_pickup_screen.dart +++ b/lib/features/booking/screens/booking_pickup_screen.dart @@ -5,6 +5,7 @@ import 'package:geocoding/geocoding.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'; @@ -123,6 +124,12 @@ class _BookingPickupScreenState extends State { draft.parcelType = _category; 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); await appState.fetchPriceEstimate(); setState(() => _loading = false); @@ -229,16 +236,14 @@ class _BookingPickupScreenState extends State { final area = '${place.subLocality ?? place.locality}, ${place.administrativeArea}'; final pincode = place.postalCode ?? ''; final fullAddress = '${place.street}, $area, $pincode'; - _showLocationConfirmationSheet(context, state, area, pincode, fullAddress); + _showLocationConfirmationSheet(context, state, area, pincode, fullAddress, position.latitude, position.longitude); } else { throw Exception('Could not determine address from coordinates.'); } } catch (e) { if (!mounted) return; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))), - ); + SnackbarUtils.showError(context, e.toString()); } }, child: Row( @@ -268,9 +273,11 @@ class _BookingPickupScreenState extends State { icon: Icons.home_rounded, label: 'Home', address: '12th Main, Indiranagar, Bengaluru, Karnataka 560038', - onSelect: (addr) => setState(() { + pincode: '560038', + onSelect: (addr, pin) => setState(() { _pickup.text = addr; final state = context.read(); + state.draft.pickupPincode = pin; _name.text = state.firstName; _mobile.text = state.phone; }), @@ -280,9 +287,11 @@ class _BookingPickupScreenState extends State { icon: Icons.business_rounded, label: 'Office', address: 'Sector 4, HSR Layout, Bengaluru, Karnataka 560102', - onSelect: (addr) => setState(() { + pincode: '560102', + onSelect: (addr, pin) => setState(() { _pickup.text = addr; final state = context.read(); + state.draft.pickupPincode = pin; _name.text = state.firstName; _mobile.text = state.phone; }), @@ -292,9 +301,11 @@ class _BookingPickupScreenState extends State { icon: Icons.favorite_rounded, label: "Mom's House", address: 'Jayanagar 4th Block, Bengaluru, Karnataka 560011', - onSelect: (addr) => setState(() { + pincode: '560011', + onSelect: (addr, pin) => setState(() { _pickup.text = addr; final state = context.read(); + state.draft.pickupPincode = pin; _name.text = state.firstName; _mobile.text = state.phone; }), @@ -516,7 +527,13 @@ class _BookingPickupScreenState extends State { ...state.savedAddresses.map((a) => Padding( padding: const EdgeInsets.only(bottom: 12), child: AppCard( - onTap: () => setState(() => _pickup.text = '${a.label}, ${a.line}'), + onTap: () { + final draft = context.read().draft; + draft.pickupPincode = a.pincode; + draft.pickupLatitude = a.latitude; + draft.pickupLongitude = a.longitude; + setState(() => _pickup.text = '${a.label}, ${a.line}'); + }, child: Row( children: [ IconBadge( @@ -543,7 +560,7 @@ class _BookingPickupScreenState extends State { ); } - 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( context: context, isScrollControlled: true, @@ -597,6 +614,9 @@ class _BookingPickupScreenState extends State { child: PrimaryButton( label: 'Confirm', onPressed: () { + state.draft.pickupPincode = pincode; + state.draft.pickupLatitude = latitude; + state.draft.pickupLongitude = longitude; setState(() { _pickup.text = fullAddress; _name.text = state.firstName; @@ -679,19 +699,21 @@ class _SavedAddressChip extends StatelessWidget { final IconData icon; final String label; final String address; - final Function(String) onSelect; + final String pincode; + final Function(String address, String pincode) onSelect; const _SavedAddressChip({ required this.icon, required this.label, required this.address, + required this.pincode, required this.onSelect, }); @override Widget build(BuildContext context) { return InkWell( - onTap: () => onSelect(address), + onTap: () => onSelect(address, pincode), borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), diff --git a/lib/features/booking/screens/booking_preview_screen.dart b/lib/features/booking/screens/booking_preview_screen.dart index 9e10514..8406748 100644 --- a/lib/features/booking/screens/booking_preview_screen.dart +++ b/lib/features/booking/screens/booking_preview_screen.dart @@ -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'; @@ -29,7 +30,7 @@ class _BookingPreviewScreenState extends State { } catch (e) { if (!mounted) return; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', '')))); + SnackbarUtils.showError(context, e.toString()); } } diff --git a/lib/features/dashboard/screens/main_shell.dart b/lib/features/dashboard/screens/main_shell.dart index 9d46532..7bd0643 100644 --- a/lib/features/dashboard/screens/main_shell.dart +++ b/lib/features/dashboard/screens/main_shell.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:doormile/main.dart'; @@ -28,9 +29,14 @@ class _MainShellState extends State { } Future _requestPermissions() async { - final status = await Permission.locationWhenInUse.status; - if (!status.isGranted) { - await Permission.locationWhenInUse.request(); + if (kIsWeb) return; + try { + final status = await Permission.locationWhenInUse.status; + if (!status.isGranted) { + await Permission.locationWhenInUse.request(); + } + } catch (e) { + debugPrint('Location permission error: $e'); } } diff --git a/lib/features/support/screens/chat_screen.dart b/lib/features/support/screens/chat_screen.dart index 09c2b6e..7b03b08 100644 --- a/lib/features/support/screens/chat_screen.dart +++ b/lib/features/support/screens/chat_screen.dart @@ -1,12 +1,30 @@ +import 'dart:async'; +import 'dart:convert'; 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_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 { 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 State createState() => _ChatScreenState(); @@ -14,25 +32,117 @@ class ChatScreen extends StatefulWidget { class _ChatScreenState extends State { final TextEditingController _messageController = TextEditingController(); - final List _messages = [ - 'Hi! I am picking up your parcel now.', - 'Please let me know if there are any specific instructions.', - ]; + final ScrollController _scrollController = ScrollController(); + final List<_ChatMessage> _messages = []; + + 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; + 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 void dispose() { _messageController.dispose(); + _scrollController.dispose(); + _channel?.sink.close(); super.dispose(); } - void _sendMessage() { - if (_messageController.text.trim().isEmpty) return; - setState(() { - _messages.add(_messageController.text.trim()); - _messageController.clear(); - }); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -43,43 +153,106 @@ class _ChatScreenState extends State { title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(widget.riderName, style: AppTheme.headlineSm.copyWith(color: Colors.white, fontSize: 18)), - Text('Doormile EV Rider', style: AppTheme.caption.copyWith(color: Colors.white70)), + Text(widget.riderName, + 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( children: [ Expanded( - child: ListView.builder( - padding: const EdgeInsets.all(16), - 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: _messages.isEmpty + ? Center( child: Text( - _messages[index], - style: AppTheme.bodyMd.copyWith(color: isRider ? AppColors.onSurface : Colors.white), + _status == _ConnStatus.connecting + ? '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( - 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( color: Colors.white, border: Border(top: BorderSide(color: AppColors.surfaceContainer)), @@ -89,6 +262,8 @@ class _ChatScreenState extends State { Expanded( child: TextField( controller: _messageController, + textInputAction: TextInputAction.send, + onSubmitted: (_) => _sendMessage(), decoration: InputDecoration( hintText: 'Message ${widget.riderName}...', border: OutlineInputBorder( @@ -97,7 +272,8 @@ class _ChatScreenState extends State { ), filled: true, fillColor: AppColors.surfaceContainerLowest, - contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + contentPadding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 14), ), ), ), diff --git a/lib/features/tracking/screens/live_tracking_screen.dart b/lib/features/tracking/screens/live_tracking_screen.dart index 8e685a2..0068e28 100644 --- a/lib/features/tracking/screens/live_tracking_screen.dart +++ b/lib/features/tracking/screens/live_tracking_screen.dart @@ -1,28 +1,157 @@ +import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.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/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/widgets/common.dart'; -class LiveTrackingScreen extends StatelessWidget { +class LiveTrackingScreen extends StatefulWidget { final Order? order; const LiveTrackingScreen({super.key, this.order}); + @override + State createState() => _LiveTrackingScreenState(); +} + +class _LiveTrackingScreenState extends State { + static const _defaultCenter = LatLng(11.02, 76.95); + + WebSocketChannel? _channel; + GoogleMapController? _mapController; + + LatLng _milerPosition = _defaultCenter; + Set _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 data; + try { + data = jsonDecode(raw as String) as Map; + } 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 Widget build(BuildContext context) { - final id = order?.id ?? 'DM-CHN-BLR-7741'; - final eta = order?.eta ?? '2:30 PM'; + final id = widget.order?.id ?? 'DM-CHN-BLR-7741'; + return Scaffold( backgroundColor: AppColors.surface, body: Stack( children: [ - // Faux map 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( child: Padding( padding: const EdgeInsets.all(12), @@ -46,7 +175,6 @@ class LiveTrackingScreen extends StatelessWidget { ), ), ), - // Bottom detail sheet Align( alignment: Alignment.bottomCenter, child: Container( @@ -81,14 +209,14 @@ class LiveTrackingScreen extends StatelessWidget { ), ), StatusChip( - label: 'In transit', + label: _status, color: AppColors.tertiary, bg: AppColors.tertiaryFixed.withValues(alpha: 0.5), ), ], ), const SizedBox(height: 4), - Text('ETA $eta', + Text('ETA $_etaLabel', style: AppTheme.labelMd.copyWith(color: AppColors.primary)), const SizedBox(height: 16), const _MileStepper(activeIndex: 2), @@ -139,7 +267,7 @@ class LiveTrackingScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Rahul Sharma', + Text(_milerName, style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), Row( children: [ @@ -195,7 +323,14 @@ class LiveTrackingScreen extends StatelessWidget { const SizedBox(width: 12), Expanded( 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), label: const Text('Chat'), style: ElevatedButton.styleFrom( @@ -216,13 +351,7 @@ class LiveTrackingScreen extends StatelessWidget { } void _toast(BuildContext context, String msg) { - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar(SnackBar( - content: Text(msg), - backgroundColor: AppColors.inverseSurface, - behavior: SnackBarBehavior.floating, - )); + SnackbarUtils.showError(context, msg); } } @@ -274,7 +403,9 @@ class _MileStepper extends StatelessWidget { height: 3, color: i == _stops.length - 1 ? 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; -} diff --git a/lib/main.dart b/lib/main.dart index 7b38251..32cf07a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -144,7 +144,17 @@ class DoormileApp extends StatelessWidget { page = const SavedAddressesScreen(); break; case Routes.chat: - page = ChatScreen(riderName: (settings.arguments as String?) ?? 'Rahul Sharma'); + final chatArgs = settings.arguments as Map? ?? {}; + page = Builder( + builder: (context) { + final appState = Provider.of(context, listen: false); + return ChatScreen( + riderName: chatArgs['milerName'] ?? 'Rider', + bookingId: chatArgs['bookingId'] ?? '', + authToken: appState.authToken, + ); + }, + ); break; default: page = const MainShell(); diff --git a/lib/shared/models/models.dart b/lib/shared/models/models.dart index 55e0314..7c501c5 100644 --- a/lib/shared/models/models.dart +++ b/lib/shared/models/models.dart @@ -9,6 +9,9 @@ class AddressEntry { final IconData icon; final Color iconBg; final Color iconColor; + final double latitude; + final double longitude; + final String pincode; const AddressEntry({ required this.label, @@ -16,6 +19,9 @@ class AddressEntry { required this.icon, this.iconBg = AppColors.surfaceContainer, this.iconColor = AppColors.onSurfaceVariant, + this.latitude = 0.0, + this.longitude = 0.0, + this.pincode = '', }); } diff --git a/lib/shared/state/app_state.dart b/lib/shared/state/app_state.dart index 9d87f53..47741c8 100644 --- a/lib/shared/state/app_state.dart +++ b/lib/shared/state/app_state.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:geolocator/geolocator.dart'; @@ -14,10 +15,13 @@ import 'package:doormile/core/theme/app_colors.dart'; /// Holds the in-progress booking the user is composing across the flow screens. class BookingDraft { String pickupAddress = ''; + String pickupPincode = ''; + double pickupLatitude = 0.0; + double pickupLongitude = 0.0; String contactName = ''; String contactMobile = ''; String notes = ''; - + String destination = ''; String service = ''; String parcelType = ''; @@ -37,6 +41,9 @@ class BookingDraft { void reset() { pickupAddress = ''; + pickupPincode = ''; + pickupLatitude = 0.0; + pickupLongitude = 0.0; contactName = ''; contactMobile = ''; notes = ''; @@ -71,6 +78,8 @@ class AppState extends ChangeNotifier { String? pendingEmail; String? authToken; bool isResettingPin = false; + bool isLoginFlow = false; + String? _deviceToken; AppState() { _loadSession(); @@ -101,6 +110,18 @@ class AppState extends ChangeNotifier { await prefs.setString('phone', phone); } + Future _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) { phone = value; notifyListeners(); @@ -195,7 +216,7 @@ class AppState extends ChangeNotifier { double lat = 0.0; double lng = 0.0; - + try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (serviceEnabled) { @@ -212,7 +233,9 @@ class AppState extends ChangeNotifier { } catch (e) { debugPrint('Error getting location for registration: $e'); } - + + final token = await _fetchDeviceToken(); + final res = await http.post( Uri.parse(ApiConstants.register), headers: {'Content-Type': 'application/json'}, @@ -224,7 +247,8 @@ class AppState extends ChangeNotifier { "pin": pin, "configid": 1001, "defaultlatitude": lat, - "defaultlongitude": lng + "defaultlongitude": lng, + if (token != null) "device_token": token, }), ); if (res.statusCode >= 200 && res.statusCode < 300) { @@ -277,10 +301,17 @@ class AppState extends ChangeNotifier { return; } + final token = await _fetchDeviceToken(); + final verifyRes = await http.post( Uri.parse(ApiConstants.verifyPin), 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) { @@ -316,8 +347,8 @@ class AppState extends ChangeNotifier { void login() { isLoggedIn = true; - // Wipe hardcoded mock data so real users only see their own actual data! - orders.clear(); + isLoginFlow = false; + orders.clear(); savedAddresses.clear(); _saveSession(); notifyListeners(); @@ -326,13 +357,13 @@ class AppState extends ChangeNotifier { Future logout() async { isLoggedIn = false; authToken = null; - // Reset in-memory user details so the previous account does not linger. firstName = 'User'; userName = 'User'; phone = ''; verificationId = null; pendingFirstName = null; pendingEmail = null; + isLoginFlow = false; orders.clear(); savedAddresses.clear(); notifyListeners(); @@ -360,6 +391,27 @@ class AppState extends ChangeNotifier { isLoadingOrders = true; 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 { print('\n=== DOORMILE API FETCH ORDERS ==='); print('Endpoint: ${ApiConstants.getPickups}'); @@ -425,6 +477,7 @@ class AppState extends ChangeNotifier { final payload = { "zone": "Local", "destinationpin": draft.dropPincode, + "pickuppincode": draft.pickupPincode, "service_type": draft.service.isNotEmpty ? draft.service : "Normal", "weight": 1.0, // Default since we removed it from UI "category": backendCategory @@ -435,6 +488,14 @@ class AppState extends ChangeNotifier { print('Payload: ${jsonEncode(payload)}'); 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 { final res = await http.post( Uri.parse(ApiConstants.checkPrice), @@ -480,8 +541,12 @@ class AppState extends ChangeNotifier { final payload = { "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, + "deliveryaddress": draft.dropAddress, + "serviceoption": draft.service, "parcels": [ { "itemcategory": backendCategory, @@ -495,6 +560,27 @@ class AppState extends ChangeNotifier { print('Payload: ${jsonEncode(payload)}'); 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( Uri.parse(ApiConstants.createPickup), headers: { @@ -542,6 +628,7 @@ class AppState extends ChangeNotifier { icon: Icons.home_rounded, iconBg: AppColors.primaryFixed, iconColor: AppColors.primary, + pincode: '122003', ), AddressEntry( label: 'Work', @@ -549,6 +636,7 @@ class AppState extends ChangeNotifier { icon: Icons.work_rounded, iconBg: AppColors.tertiaryFixed, iconColor: AppColors.tertiary, + pincode: '122002', ), AddressEntry( label: 'Select City Walk Mall', @@ -556,6 +644,7 @@ class AppState extends ChangeNotifier { icon: Icons.history_rounded, iconBg: AppColors.surfaceContainerHigh, iconColor: AppColors.onSurfaceVariant, + pincode: '110017', ), ]; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 79c1265..cb9ba9f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import firebase_auth import firebase_core +import firebase_messaging import geolocator_apple import package_info_plus import shared_preferences_foundation @@ -15,6 +16,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 9ebf0de..3db0889 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,6 +201,30 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -238,6 +262,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct dev" description: flutter @@ -344,6 +376,54 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -592,6 +672,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -677,6 +765,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -813,6 +909,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 24625aa..9909370 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,9 @@ dependencies: geolocator: ^14.0.2 url_launcher: ^6.3.2 geocoding: ^4.0.0 + google_maps_flutter: ^2.9.0 + web_socket_channel: ^3.0.1 + firebase_messaging: 16.3.0 dev_dependencies: flutter_test: