2026-04-11

This commit is contained in:
Sarankumar committed 2026-04-11 10:21:31 +05:30
commit 27e27fb70b
270 files changed
+30296

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
class AppAssets {
static const String taxgildelogo = 'assets/images/taxglidelogo.png';
static const String taxgildelogoauth = 'assets/images/taxglidelogoauth.png';
static const String homepageheader = 'assets/images/haeder_image.jpg';
static const String profilebanner = 'assets/images/profilebanner.jpg';
static const String backgroundimages = 'assets/images/backgroundimages.png';
static const String back = "assets/images/homepagebackground.jpg";
static const String image = "assets/images/home.png";
static const String generalchatimage = "assets/images/generalchatimage.png";
static const String maincontroller = "assets/images/taxglidewhitelogo.png";
static const String generalchatsvg = "assets/images/backgroudimagesvg.svg";
}
+17
View File
@@ -0,0 +1,17 @@
import 'dart:ui';
class AppColors {
static const Color backgroundgrainetflashtop = Color(0xFFEBC894);
static const Color backgroundgrainetflashbottom = Color(0xFFB49EF4);
static const Color authheading = Color(0xFF111827);
static const Color authleading = Color(0xFF6C7278);
static const Color authtermsandcondition = Color(0xFFC9861D);
static const Color authsignup = Color(0xFF4DA526);
static const Color authchange = Color(0xFFFF0004);
static const Color commanbutton = Color(0xFF5F297B);
static const Color homepagekycdetails = Color(0xFF239F5B);
static const Color homepagekycdetailsorange = Color(0xFFF68945);
static const Color homepageserivcetext = Color(0xFF3F3F3F);
static const Color black = Color(0xFF000000);
static const Color white = Color(0xFFFFFFFF);
}
+70
View File
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:taxglide/consts/app_colors.dart';
class CommanButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final String? backgroundImage; // optional bg image
final Widget? prefixIcon; // optional prefix icon
const CommanButton({
super.key,
required this.text,
required this.onPressed,
this.backgroundImage,
this.prefixIcon,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 57,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: AppColors.commanbutton, // base color (visible always)
image: backgroundImage != null
? DecorationImage(
image: AssetImage(backgroundImage!),
fit: BoxFit.cover,
// 🔥 THIS MAKES BOTH IMAGE + COLOR VISIBLE
colorFilter: ColorFilter.mode(
AppColors.commanbutton.withOpacity(0.5),
BlendMode.srcATop,
),
)
: null,
),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (prefixIcon != null) ...[
prefixIcon!,
const SizedBox(width: 8),
],
Text(
text,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 16,
height: 1.4,
letterSpacing: 0.03 * 16,
color: Colors.white,
),
),
],
),
),
),
),
);
}
}
+31
View File
@@ -0,0 +1,31 @@
// ignore_for_file: use_super_parameters
import 'package:flutter/material.dart';
class CommonContainerAuth extends StatelessWidget {
final Widget child;
const CommonContainerAuth({Key? key, required this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(left: 16, right: 16),
padding: const EdgeInsets.only(top: 31, right: 18, bottom: 31, left: 18),
decoration: BoxDecoration(
color: const Color(0x99FFFFFF), // #FFFFFF with 60% opacity (99 in hex)
border: Border.all(color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: child,
);
}
}
+101
View File
@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:dropdown_textfield/dropdown_textfield.dart';
class CommanDropdown extends StatefulWidget {
final List<String> items;
final String? value;
final Function(String?) onChanged;
final String hint;
final bool useTextFieldType;
const CommanDropdown({
Key? key,
required this.items,
this.value,
required this.onChanged,
this.hint = 'Select an option',
this.useTextFieldType = false,
}) : super(key: key);
@override
State<CommanDropdown> createState() => _CommanDropdownState();
}
class _CommanDropdownState extends State<CommanDropdown> {
@override
Widget build(BuildContext context) {
if (widget.useTextFieldType) {
return Container(
width: double.infinity,
height: 56,
decoration: _boxDecoration(),
child: DropDownTextField(
textFieldDecoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 16),
),
clearOption: false,
dropDownList: widget.items
.map((e) => DropDownValueModel(name: e, value: e))
.toList(),
dropDownItemCount: widget.items.length > 6 ? 6 : widget.items.length,
// dropdownHeight: 200, // Limit the dropdown height
isEnabled: true,
onChanged: (value) {
widget.onChanged(value?.value);
},
),
);
} else {
return Container(
width: double.infinity,
height: 56,
decoration: _boxDecoration(),
padding: const EdgeInsets.symmetric(horizontal: 0),
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value: widget.value,
isExpanded: true,
hint: Text(widget.hint, style: const TextStyle(color: Colors.grey)),
items: widget.items
.map(
(item) => DropdownMenuItem(
value: item,
child: Text(item, style: const TextStyle(fontSize: 14)),
),
)
.toList(),
onChanged: widget.onChanged,
buttonStyleData: const ButtonStyleData(height: 56),
iconStyleData: const IconStyleData(
icon: Icon(Icons.arrow_drop_down),
),
dropdownStyleData: DropdownStyleData(
maxHeight: 300, // Limit dropdown menu height
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6)),
),
menuItemStyleData: const MenuItemStyleData(
height: 48, // Set individual item height
),
),
),
);
}
}
BoxDecoration _boxDecoration() {
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFDFDFDF), width: 1),
boxShadow: [
BoxShadow(
color: const Color(0x40BDBDBD),
blurRadius: 7,
offset: const Offset(0, 1),
),
],
);
}
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
class CommanImageBox extends StatelessWidget {
final VoidCallback onTap;
final String text;
final IconData icon;
const CommanImageBox({
super.key,
required this.onTap,
this.text = 'Upload Image',
this.icon = Icons.cloud_upload_outlined,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: double.infinity,
height: 117,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFCFCFCF), width: 1),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 1),
blurRadius: 7,
),
],
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 32, color: const Color(0xFF5F297B)),
const SizedBox(height: 8),
Text(
text,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF5F297B),
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
}
+80
View File
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
class CommonInfoPopup {
/// 🔹 Show a reusable popup dialog
static void show({
required BuildContext context,
required String title,
required String content,
}) {
final ScrollController scrollController = ScrollController();
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext ctx) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
backgroundColor: Colors.white,
titlePadding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
contentPadding: const EdgeInsets.fromLTRB(20, 0, 20, 10),
title: Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.black87,
),
),
content: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: ScrollbarTheme(
data: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all(
const Color(0xFF6A4BFC),
), // 💜 Custom scrollbar color
trackColor: WidgetStateProperty.all(Colors.transparent),
radius: const Radius.circular(8),
thickness: WidgetStateProperty.all(4),
),
child: Scrollbar(
controller: scrollController,
thumbVisibility: true,
child: SingleChildScrollView(
controller: scrollController,
child: Text(
content,
textAlign: TextAlign.justify,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontSize: 14,
height: 1.6,
color: Colors.black54,
),
),
),
),
),
),
actionsPadding: const EdgeInsets.only(right: 10, bottom: 8),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text(
"Close",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontSize: 14,
color: Color(0xFF6A4BFC),
),
),
),
],
);
},
);
}
}
+82
View File
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:taxglide/consts/app_colors.dart';
import 'package:taxglide/model/serivce_list_model.dart';
import 'package:taxglide/view/Main_controller/main_controller.dart';
import 'package:taxglide/view/screens/serivce_request_screen.dart';
class CommonServiceItem extends StatelessWidget {
final ServiceListModel service;
final int sourceTabIndex; // ✅ New param to know from where user clicked
const CommonServiceItem({
super.key,
required this.service,
required this.sourceTabIndex,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () {
Get.offAll(
() => MainController(
child: ServiceRequestScreen(
id: service.id,
service: service.service,
icon: service.icon,
),
initialIndex: sourceTabIndex, // 0 for Home, 1 for Services
sourceTabIndex:
sourceTabIndex, // to remember where it came from
),
);
},
child: Container(
width: 110,
height: 84,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFE1E1E1), width: 1),
boxShadow: const [
BoxShadow(
color: Color(0x66757575),
offset: Offset(0, 1),
blurRadius: 7.3,
),
],
),
padding: const EdgeInsets.all(8),
child: Center(
child: Image.network(
service.icon,
width: 42,
height: 42,
fit: BoxFit.contain,
),
),
),
),
const SizedBox(height: 16),
Text(
service.service,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 13,
height: 1.3,
letterSpacing: 0.02,
color: AppColors.homepageserivcetext,
),
),
],
);
}
}
+127
View File
@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:taxglide/consts/app_colors.dart';
import 'package:flutter/services.dart';
class CommanTextFormField extends StatelessWidget {
final String? hintText;
final TextEditingController? controller;
final TextInputType? keyboardType;
final bool obscureText;
final bool readOnly;
final String? Function(String?)? validator;
final IconData? prefixIcon;
final bool hasError;
final Function(String)? onChanged;
final FocusNode? focusNode;
final VoidCallback? onTap;
/// ✅ NEW
final List<TextInputFormatter>? inputFormatters;
final TextCapitalization textCapitalization;
const CommanTextFormField({
Key? key,
this.hintText,
this.controller,
this.keyboardType,
this.obscureText = false,
this.readOnly = false,
this.validator,
this.prefixIcon,
this.hasError = false,
this.onChanged,
this.focusNode,
this.onTap,
/// ✅ DEFAULTS
this.inputFormatters,
this.textCapitalization = TextCapitalization.none,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 56,
child: TextFormField(
focusNode: focusNode,
controller: controller,
keyboardType: keyboardType,
obscureText: obscureText,
validator: validator,
onChanged: onChanged,
onTap: onTap,
readOnly: readOnly,
/// ✅ APPLIED HERE
inputFormatters: inputFormatters,
textCapitalization: textCapitalization,
cursorColor: readOnly ? Colors.transparent : AppColors.authleading,
decoration: InputDecoration(
hintText: hintText,
filled: true,
fillColor: readOnly ? const Color(0xFFF6F6F6) : Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
prefixIcon: prefixIcon != null
? Icon(prefixIcon, color: AppColors.authleading)
: null,
hintStyle: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 11.31,
height: 1.4,
letterSpacing: 0.03,
color: hasError
? Colors.red.withOpacity(0.6)
: Colors.grey.shade500,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(
color: hasError
? Colors.red
: readOnly
? Colors.grey.shade300
: const Color(0xFFDFDFDF),
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(
color: hasError
? Colors.red
: readOnly
? Colors.grey.shade300
: const Color(0xFFDFDFDF),
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(
color: hasError
? Colors.red
: readOnly
? Colors.grey.shade400
: Colors.blue,
width: 1,
),
),
),
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 11.31,
height: 1.4,
letterSpacing: 0.03,
color: readOnly ? Colors.grey.shade600 : AppColors.authleading,
),
),
);
}
}
+240
View File
@@ -0,0 +1,240 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// Common WebSocket for user-based communication (not chatId-based)
class CommonWebSocketService {
static final CommonWebSocketService _instance =
CommonWebSocketService._internal();
factory CommonWebSocketService() => _instance;
CommonWebSocketService._internal();
WebSocketChannel? _channel;
StreamSubscription? _subscription;
Timer? _pingTimer;
Timer? _reconnectTimer;
bool _isConnected = false;
bool _isIntentionalClose = false;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 5;
static const Duration _reconnectDelay = Duration(seconds: 3);
// Callbacks
Function(Map<String, dynamic>)? onMessageReceived;
Function()? onConnected;
Function()? onDisconnected;
Function(dynamic error)? onError;
String? _userId;
String? _wsUrl;
/// Check connection status
bool get isConnected => _isConnected;
/// Current user ID
String? get currentUserId => _userId;
/// Connect WS using LOCAL USER ID
Future<void> connect({
required String userId,
String baseUrl = "wss://taxglide.amrithaa.net:443/reverb",
String appKey = "2yj0lyzc9ylw2h03ts6i",
}) async {
try {
await disconnect();
_userId = userId;
_wsUrl = "$baseUrl/app/$appKey?protocol=7&client=js&version=1.0";
_isIntentionalClose = false;
_reconnectAttempts = 0;
await _establishConnection();
} catch (e) {
debugPrint("❌ Connection error: $e");
onError?.call(e);
_scheduleReconnect();
}
}
Future<void> _establishConnection() async {
try {
debugPrint("🔌 Connecting to: $_wsUrl");
_channel = WebSocketChannel.connect(Uri.parse(_wsUrl!));
_subscription = _channel!.stream.listen(
_handleMessage,
onError: _handleError,
onDone: _handleDisconnection,
cancelOnError: false,
);
await Future.delayed(const Duration(milliseconds: 500));
_isConnected = true;
_reconnectAttempts = 0;
debugPrint("✅ WebSocket connected");
_subscribeToUserChannel();
_startPingTimer();
onConnected?.call();
} catch (e) {
debugPrint("❌ Failed to establish connection: $e");
_isConnected = false;
rethrow;
}
}
/// Handle incoming messages
void _handleMessage(dynamic data) {
try {
debugPrint("📨 Received: $data");
final Map<String, dynamic> message = json.decode(data);
if (message['event'] == 'pusher:ping') {
_sendPong();
return;
}
if (message['event'] == 'pusher:connection_established') {
debugPrint("🔗 Pusher connection established");
return;
}
if (message['event'] == 'pusher_internal:subscription_succeeded') {
debugPrint("✅ User channel subscribed");
return;
}
if (message['event'] != null &&
!message['event'].toString().startsWith('pusher')) {
onMessageReceived?.call(message);
}
} catch (e) {
debugPrint("❌ Error parsing message: $e");
onError?.call(e);
}
}
void _handleError(dynamic error) {
debugPrint("❌ WebSocket error: $error");
_isConnected = false;
onError?.call(error);
if (!_isIntentionalClose) {
_scheduleReconnect();
}
}
void _handleDisconnection() {
debugPrint("🔌 Disconnected");
_isConnected = false;
_stopPingTimer();
onDisconnected?.call();
if (!_isIntentionalClose) {
_scheduleReconnect();
}
}
/// Subscribe to USER CHANNEL
void _subscribeToUserChannel() {
if (_userId == null) return;
final subscribeMessage = {
'event': 'pusher:subscribe',
'data': {'channel': 'user-chat-notification.$_userId'},
};
_sendMessage(subscribeMessage);
debugPrint("📡 Subscribing to user.$_userId");
}
void _sendPong() {
final pong = {'event': 'pusher:pong', 'data': {}};
_sendMessage(pong);
}
void _startPingTimer() {
_pingTimer?.cancel();
_pingTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
if (_isConnected) debugPrint("💓 WS heartbeat");
});
}
void _stopPingTimer() {
_pingTimer?.cancel();
_pingTimer = null;
}
void _scheduleReconnect() {
if (_isIntentionalClose) return;
if (_reconnectAttempts >= _maxReconnectAttempts) {
onError?.call(
"Failed to reconnect after $_maxReconnectAttempts attempts",
);
return;
}
_reconnectTimer?.cancel();
_reconnectAttempts++;
debugPrint(
"🔄 Retry $_reconnectAttempts in ${_reconnectDelay.inSeconds}s...",
);
_reconnectTimer = Timer(_reconnectDelay, () async {
if (!_isIntentionalClose && _wsUrl != null && _userId != null) {
await _establishConnection();
}
});
}
/// Send message
void _sendMessage(Map<String, dynamic> message) {
if (!_isConnected || _channel == null) {
debugPrint("⚠️ Cannot send: Not connected");
return;
}
try {
final jsonMsg = json.encode(message);
_channel!.sink.add(jsonMsg);
debugPrint("📤 Sent: $jsonMsg");
} catch (e) {
debugPrint("❌ Send error: $e");
}
}
/// Send event to backend
void sendEvent({
required String eventName,
required Map<String, dynamic> data,
}) {
final msg = {
'event': eventName,
'data': data,
'channel': 'user-chat-notification.$_userId',
};
_sendMessage(msg);
}
Future<void> disconnect() async {
_isIntentionalClose = true;
_reconnectTimer?.cancel();
_stopPingTimer();
await _subscription?.cancel();
await _channel?.sink.close();
_channel = null;
_subscription = null;
_isConnected = false;
_userId = null;
debugPrint("🔌 WS disconnected intentionally");
}
}
+240
View File
@@ -0,0 +1,240 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// Common WebSocket for user-based communication (not chatId-based)
class DetailsWebscokect {
static final DetailsWebscokect _instance =
DetailsWebscokect._internal();
factory DetailsWebscokect() => _instance;
DetailsWebscokect._internal();
WebSocketChannel? _channel;
StreamSubscription? _subscription;
Timer? _pingTimer;
Timer? _reconnectTimer;
bool _isConnected = false;
bool _isIntentionalClose = false;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 5;
static const Duration _reconnectDelay = Duration(seconds: 3);
// Callbacks
Function(Map<String, dynamic>)? onMessageReceived;
Function()? onConnected;
Function()? onDisconnected;
Function(dynamic error)? onError;
String? _userId;
String? _wsUrl;
/// Check connection status
bool get isConnected => _isConnected;
/// Current user ID
String? get currentUserId => _userId;
/// Connect WS using LOCAL USER ID
Future<void> connect({
required String userId,
String baseUrl = "wss://taxglide.amrithaa.net:443/reverb",
String appKey = "2yj0lyzc9ylw2h03ts6i",
}) async {
try {
await disconnect();
_userId = userId;
_wsUrl = "$baseUrl/app/$appKey?protocol=7&client=js&version=1.0";
_isIntentionalClose = false;
_reconnectAttempts = 0;
await _establishConnection();
} catch (e) {
debugPrint("❌ Connection error: $e");
onError?.call(e);
_scheduleReconnect();
}
}
Future<void> _establishConnection() async {
try {
debugPrint("🔌 Connecting to: $_wsUrl");
_channel = WebSocketChannel.connect(Uri.parse(_wsUrl!));
_subscription = _channel!.stream.listen(
_handleMessage,
onError: _handleError,
onDone: _handleDisconnection,
cancelOnError: false,
);
await Future.delayed(const Duration(milliseconds: 500));
_isConnected = true;
_reconnectAttempts = 0;
debugPrint("✅ WebSocket connected");
_subscribeToUserChannel();
_startPingTimer();
onConnected?.call();
} catch (e) {
debugPrint("❌ Failed to establish connection: $e");
_isConnected = false;
rethrow;
}
}
/// Handle incoming messages
void _handleMessage(dynamic data) {
try {
debugPrint("📨 Received: $data");
final Map<String, dynamic> message = json.decode(data);
if (message['event'] == 'pusher:ping') {
_sendPong();
return;
}
if (message['event'] == 'pusher:connection_established') {
debugPrint("🔗 Pusher connection established");
return;
}
if (message['event'] == 'pusher_internal:subscription_succeeded') {
debugPrint("✅ User channel subscribed");
return;
}
if (message['event'] != null &&
!message['event'].toString().startsWith('pusher')) {
onMessageReceived?.call(message);
}
} catch (e) {
debugPrint("❌ Error parsing message: $e");
onError?.call(e);
}
}
void _handleError(dynamic error) {
debugPrint("❌ WebSocket error: $error");
_isConnected = false;
onError?.call(error);
if (!_isIntentionalClose) {
_scheduleReconnect();
}
}
void _handleDisconnection() {
debugPrint("🔌 Disconnected");
_isConnected = false;
_stopPingTimer();
onDisconnected?.call();
if (!_isIntentionalClose) {
_scheduleReconnect();
}
}
/// Subscribe to USER CHANNEL
void _subscribeToUserChannel() {
if (_userId == null) return;
final subscribeMessage = {
'event': 'pusher:subscribe',
'data': {'channel': 'user-chat-notification.$_userId'},
};
_sendMessage(subscribeMessage);
debugPrint("📡 Subscribing to user.$_userId");
}
void _sendPong() {
final pong = {'event': 'pusher:pong', 'data': {}};
_sendMessage(pong);
}
void _startPingTimer() {
_pingTimer?.cancel();
_pingTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
if (_isConnected) debugPrint("💓 WS heartbeat");
});
}
void _stopPingTimer() {
_pingTimer?.cancel();
_pingTimer = null;
}
void _scheduleReconnect() {
if (_isIntentionalClose) return;
if (_reconnectAttempts >= _maxReconnectAttempts) {
onError?.call(
"Failed to reconnect after $_maxReconnectAttempts attempts",
);
return;
}
_reconnectTimer?.cancel();
_reconnectAttempts++;
debugPrint(
"🔄 Retry $_reconnectAttempts in ${_reconnectDelay.inSeconds}s...",
);
_reconnectTimer = Timer(_reconnectDelay, () async {
if (!_isIntentionalClose && _wsUrl != null && _userId != null) {
await _establishConnection();
}
});
}
/// Send message
void _sendMessage(Map<String, dynamic> message) {
if (!_isConnected || _channel == null) {
debugPrint("⚠️ Cannot send: Not connected");
return;
}
try {
final jsonMsg = json.encode(message);
_channel!.sink.add(jsonMsg);
debugPrint("📤 Sent: $jsonMsg");
} catch (e) {
debugPrint("❌ Send error: $e");
}
}
/// Send event to backend
void sendEvent({
required String eventName,
required Map<String, dynamic> data,
}) {
final msg = {
'event': eventName,
'data': data,
'channel': 'user-chat-notification.$_userId',
};
_sendMessage(msg);
}
Future<void> disconnect() async {
_isIntentionalClose = true;
_reconnectTimer?.cancel();
_stopPingTimer();
await _subscription?.cancel();
await _channel?.sink.close();
_channel = null;
_subscription = null;
_isConnected = false;
_userId = null;
debugPrint("🔌 WS disconnected intentionally");
}
}
+539
View File
@@ -0,0 +1,539 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:open_filex/open_filex.dart';
import 'package:gal/gal.dart';
class DownloadHelper {
static const String API_BASE_URL = "https://www.taxglide.amrithaa.net/api/";
/// Request storage permission based on Android version
static Future<bool> requestStoragePermission() async {
if (!Platform.isAndroid) return true;
try {
final androidInfo = await DeviceInfoPlugin().androidInfo;
final sdkInt = androidInfo.version.sdkInt;
print("📱 Android SDK: $sdkInt");
// Android 13+ (API 33+)
if (sdkInt >= 33) {
final photoStatus = await Permission.photos.request();
final videoStatus = await Permission.videos.request();
print("✅ Android 13+ - Media permissions requested");
return photoStatus.isGranted || videoStatus.isGranted;
}
// Android 11-12 (API 30-32)
if (sdkInt >= 30) {
if (await Permission.manageExternalStorage.isGranted) {
return true;
}
var status = await Permission.manageExternalStorage.request();
if (status.isGranted) return true;
status = await Permission.storage.request();
return status.isGranted;
}
// Android 10 and below (API 29 and below)
final status = await Permission.storage.request();
return status.isGranted;
} catch (e) {
print("❌ Permission error: $e");
return false;
}
}
/// ⭐ NEW: Get TaxGlide folder in PUBLIC Downloads
static Future<Directory> getTaxGlideFolder() async {
try {
await requestStoragePermission();
if (Platform.isAndroid) {
// Use public Downloads folder for all Android versions
final publicTaxGlide = Directory(
'/storage/emulated/0/Download/TaxGlide',
);
if (!await publicTaxGlide.exists()) {
await publicTaxGlide.create(recursive: true);
print(
"✅ TaxGlide folder created in public Downloads: ${publicTaxGlide.path}",
);
}
return publicTaxGlide;
} else {
// iOS - use app documents
final baseDirectory = await getApplicationDocumentsDirectory();
final taxGlideFolder = Directory('${baseDirectory.path}/TaxGlide');
if (!await taxGlideFolder.exists()) {
await taxGlideFolder.create(recursive: true);
}
return taxGlideFolder;
}
} catch (e) {
print("❌ Error creating TaxGlide folder: $e");
// Fallback to app-specific storage if public storage fails
final fallbackDir = await getExternalStorageDirectory();
final taxGlideFolder = Directory('${fallbackDir!.path}/TaxGlide');
if (!await taxGlideFolder.exists()) {
await taxGlideFolder.create(recursive: true);
print("⚠️ Using app-specific storage as fallback");
}
return taxGlideFolder;
}
}
/// ⭐ Get Received folder in PUBLIC Downloads
static Future<Directory> getReceivedFolder() async {
final taxGlideFolder = await getTaxGlideFolder();
final receivedFolder = Directory('${taxGlideFolder.path}/Received');
if (!await receivedFolder.exists()) {
await receivedFolder.create(recursive: true);
print("✅ Received folder created: ${receivedFolder.path}");
}
return receivedFolder;
}
/// ⭐ Get Send folder in PUBLIC Downloads
static Future<Directory> getSendFolder() async {
final taxGlideFolder = await getTaxGlideFolder();
final sendFolder = Directory('${taxGlideFolder.path}/Send');
if (!await sendFolder.exists()) {
await sendFolder.create(recursive: true);
print("✅ Send folder created: ${sendFolder.path}");
}
return sendFolder;
}
/// Save file to Gallery (for images/videos only)
static Future<bool> saveToGallery(String filePath) async {
try {
final file = File(filePath);
if (!await file.exists()) {
print("❌ File does not exist: $filePath");
return false;
}
final fileName = filePath.split('/').last;
// Request permissions
final hasPermission = await requestStoragePermission();
if (!hasPermission) {
print("⚠️ No permission to save to gallery");
return false;
}
// Only save images/videos to Gallery
if (isImageFile(filePath)) {
await Gal.putImage(filePath, album: 'TaxGlide');
print("✅ Image saved to Gallery: $fileName");
return true;
} else if (isVideoFile(filePath)) {
await Gal.putVideo(filePath, album: 'TaxGlide');
print("✅ Video saved to Gallery: $fileName");
return true;
}
return false;
} catch (e) {
print("❌ Error saving to gallery: $e");
return false;
}
}
/// Build download URL
static String buildDownloadUrl(String filePath) {
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
return filePath;
}
return '${API_BASE_URL}chat/download/$filePath';
}
/// Check if file exists in Received folder
static Future<String?> checkFileExistsInReceived(String url) async {
try {
final fileName = url.contains('/')
? url.split('/').last.split('?').first
: url.split('?').first;
final receivedFolder = await getReceivedFolder();
final filePath = '${receivedFolder.path}/$fileName';
final file = File(filePath);
if (await file.exists()) {
print("✅ File already exists in Received: $filePath");
return filePath;
}
return null;
} catch (e) {
print("❌ Error checking file existence: $e");
return null;
}
}
/// Backward compatibility
static Future<String?> checkFileExists(String url) async {
return await checkFileExistsInReceived(url);
}
/// ⭐ Download file to Received folder (now in public Downloads) and Gallery
static Future<Map<String, dynamic>> downloadFileToReceived(String url) async {
try {
final fullUrl = buildDownloadUrl(url);
print("🔗 Downloading from: $fullUrl");
// Check if file already exists
final existingFilePath = await checkFileExistsInReceived(fullUrl);
if (existingFilePath != null) {
print("📂 File already exists, opening it...");
await openDownloadedFile(existingFilePath);
return {
'filePath': existingFilePath,
'isNewDownload': false,
'success': true,
};
}
// Request permissions
await requestStoragePermission();
// Get filename and prepare save path
String fileName = fullUrl.split('/').last.split('?').first;
fileName = fileName.replaceAll(RegExp(r'[^\w\s\-\.]'), '_');
final receivedFolder = await getReceivedFolder();
final savePath = "${receivedFolder.path}/$fileName";
print("Download Started: Downloading $fileName...");
// Download file to Received folder (now in public Downloads)
Dio dio = Dio();
await dio.download(
fullUrl,
savePath,
onReceiveProgress: (received, total) {
if (total != -1) {
final progress = (received / total * 100).toStringAsFixed(0);
print("📊 Download progress: $progress%");
}
},
);
// Also save images/videos to Gallery
bool savedToGallery = false;
try {
savedToGallery = await saveToGallery(savePath);
} catch (e) {
print("⚠️ Could not save to gallery: $e");
}
if (savedToGallery) {
print(
"Download Complete: $fileName saved to Gallery & Downloads/TaxGlide/Received",
);
} else {
print(
"Download Complete: $fileName saved to Downloads/TaxGlide/Received",
);
}
print("✅ File downloaded to: $savePath");
// Open the downloaded file
await openDownloadedFile(savePath);
return {
'filePath': savePath,
'isNewDownload': true,
'success': true,
'savedToGallery': savedToGallery,
};
} catch (e) {
print("❌ Download error: $e");
print("Download Failed: Error: ${e.toString()}");
return {'success': false, 'error': e.toString()};
}
}
/// Backward compatibility
static Future<Map<String, dynamic>> downloadFile(String url) async {
return await downloadFileToReceived(url);
}
/// ⭐ Save file to Send folder (now in public Downloads)
static Future<String> saveToSendFolder(File file) async {
try {
if (!await file.exists()) {
throw Exception("Source file does not exist: ${file.path}");
}
final sendFolder = await getSendFolder();
final originalFileName = file.path.split('/').last;
final timestamp = DateTime.now().millisecondsSinceEpoch;
final fileName = '${timestamp}_$originalFileName';
final savePath = '${sendFolder.path}/$fileName';
final savedFile = await file.copy(savePath);
print("✅ File saved to Send folder (Downloads/TaxGlide/Send): $savePath");
// Also save images/videos to Gallery
try {
await saveToGallery(savePath);
} catch (e) {
print("⚠️ Could not save to gallery: $e");
}
return savedFile.path;
} catch (e) {
print("❌ Error saving to Send folder: $e");
print("⚠️ Using original file path: ${file.path}");
return file.path;
}
}
/// Save multiple files to Send folder
static Future<List<File>> saveMultipleToSendFolder(List<File> files) async {
List<File> savedFiles = [];
for (File file in files) {
try {
final savedPath = await saveToSendFolder(file);
savedFiles.add(File(savedPath));
} catch (e) {
print("❌ Error saving file: ${file.path}, using original");
savedFiles.add(file);
}
}
return savedFiles;
}
/// Open file with appropriate viewer
static Future<void> openDownloadedFile(String path) async {
print("🔓 Opening file: $path");
try {
final file = File(path);
if (!await file.exists()) {
print("❌ File not found at $path");
print("Error: File not found");
return;
}
final result = await OpenFilex.open(path);
if (result.type != ResultType.done) {
print("⚠️ Could not open file: ${result.message}");
print("Notice: File saved but no app found to open it");
}
} catch (e) {
print("❌ Error opening file: $e");
print("Error: Could not open file");
}
}
/// Get all files from Send folder
static Future<List<File>> getSentFiles() async {
try {
final sendFolder = await getSendFolder();
if (await sendFolder.exists()) {
return sendFolder.listSync().whereType<File>().toList()..sort(
(a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()),
);
}
return [];
} catch (e) {
print("❌ Error getting sent files: $e");
return [];
}
}
/// Get all files from Received folder
static Future<List<File>> getReceivedFiles() async {
try {
final receivedFolder = await getReceivedFolder();
if (await receivedFolder.exists()) {
return receivedFolder.listSync().whereType<File>().toList()..sort(
(a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()),
);
}
return [];
} catch (e) {
print("❌ Error getting received files: $e");
return [];
}
}
/// Delete a file
static Future<bool> deleteFile(String filePath) async {
try {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
print("✅ File deleted: $filePath");
print("File Deleted: File removed successfully");
return true;
}
return false;
} catch (e) {
print("❌ Error deleting file: $e");
return false;
}
}
/// Clear all files from Send folder
static Future<void> clearSendFolder() async {
try {
final files = await getSentFiles();
for (final file in files) {
await file.delete();
}
print("✅ Send folder cleared: ${files.length} files deleted");
} catch (e) {
print("❌ Error clearing Send folder: $e");
}
}
/// Clear all files from Received folder
static Future<void> clearReceivedFolder() async {
try {
final files = await getReceivedFiles();
for (final file in files) {
await file.delete();
}
print("✅ Received folder cleared: ${files.length} files deleted");
} catch (e) {
print("❌ Error clearing Received folder: $e");
}
}
/// Get file size in human-readable format
static Future<String> getFileSize(File file) async {
try {
final bytes = await file.length();
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) {
return '${(bytes / 1024).toStringAsFixed(2)} KB';
}
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
} catch (e) {
return 'Unknown';
}
}
/// Get file extension
static String getFileExtension(String path) {
return path.split('.').last.toLowerCase();
}
/// Check if file is image
static bool isImageFile(String path) {
final ext = getFileExtension(path);
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].contains(ext);
}
/// Check if file is document
static bool isDocumentFile(String path) {
final ext = getFileExtension(path);
return [
'pdf',
'doc',
'docx',
'xls',
'xlsx',
'txt',
'ppt',
'pptx',
'rtf',
'odt',
].contains(ext);
}
/// Check if file is video
static bool isVideoFile(String path) {
final ext = getFileExtension(path);
return ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].contains(ext);
}
/// Check if file is audio
static bool isAudioFile(String path) {
final ext = getFileExtension(path);
return ['mp3', 'wav', 'aac', 'flac', 'ogg', 'm4a', 'wma'].contains(ext);
}
/// Get file icon based on type
static String getFileIcon(String path) {
if (isImageFile(path)) return '🖼️';
if (isVideoFile(path)) return '🎥';
if (isAudioFile(path)) return '🎵';
if (isDocumentFile(path)) {
final ext = getFileExtension(path);
if (ext == 'pdf') return '📄';
if (['doc', 'docx'].contains(ext)) return '📝';
if (['xls', 'xlsx'].contains(ext)) return '📊';
if (['ppt', 'pptx'].contains(ext)) return '📽️';
}
return '📎';
}
/// Get storage info
static Future<Map<String, String>> getStorageInfo() async {
try {
final taxGlideFolder = await getTaxGlideFolder();
final sentFiles = await getSentFiles();
final receivedFiles = await getReceivedFiles();
int totalSentSize = 0;
int totalReceivedSize = 0;
for (final file in sentFiles) {
totalSentSize += await file.length();
}
for (final file in receivedFiles) {
totalReceivedSize += await file.length();
}
return {
'taxGlidePath': taxGlideFolder.path,
'sentFiles': sentFiles.length.toString(),
'receivedFiles': receivedFiles.length.toString(),
'sentSize': _formatBytes(totalSentSize),
'receivedSize': _formatBytes(totalReceivedSize),
'totalSize': _formatBytes(totalSentSize + totalReceivedSize),
};
} catch (e) {
print("❌ Error getting storage info: $e");
return {};
}
}
static String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
}
}
+139
View File
@@ -0,0 +1,139 @@
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
/// 🔥 FCM Token Manager - Force regenerates token on logout/login
class FcmTokenManager {
static final FcmTokenManager _instance = FcmTokenManager._internal();
factory FcmTokenManager() => _instance;
FcmTokenManager._internal();
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
/// 🔑 Get current FCM token
Future<String?> getCurrentToken() async {
try {
if (Platform.isIOS) {
final apns = await _messaging.getAPNSToken();
if (apns == null) {
debugPrint('⚠️ APIs Token missing on iOS - Skipping FCM getToken');
return null;
}
}
debugPrint('🔄 Getting current FCM token...');
final token = await _messaging.getToken();
debugPrint('✅ Current FCM Token: $token');
return token;
} catch (e) {
debugPrint('❌ Error getting FCM token: $e');
return null;
}
}
/// 🔄 Force regenerate FCM token (deletes old and creates new)
/// Call this BEFORE login to ensure fresh token
/// 🔄 Force regenerate FCM token (deletes old and creates new)
Future<String?> regenerateToken() async {
try {
if (Platform.isIOS) {
final apns = await _messaging.getAPNSToken();
if (apns == null) {
debugPrint(
'⚠️ APIs Token missing on iOS - Cannot regenerate FCM token',
);
return null;
}
}
debugPrint('🔄 Starting FCM token regeneration...');
// Step 1: Get old token
String? oldToken;
try {
oldToken = await _messaging.getToken();
debugPrint('📋 OLD FCM Token: $oldToken');
} catch (e) {
debugPrint('⚠️ Could not get old token: $e');
}
// Step 2: Delete the old token
try {
debugPrint('🗑️ Deleting old FCM token...');
await _messaging.deleteToken();
debugPrint('✅ Old token deleted');
} catch (e) {
debugPrint('⚠️ Error deleting old token: $e');
}
// Step 3: Wait
await Future.delayed(const Duration(milliseconds: 800));
// Step 4: Request permission
await _messaging.requestPermission(alert: true, badge: true, sound: true);
// Step 5: Generate new token
debugPrint('🔄 Generating new FCM token...');
final newToken = await _messaging.getToken();
if (newToken != null) {
debugPrint('✅ NEW FCM Token: $newToken');
} else {
debugPrint('⚠️ Failed to generate new token');
}
return newToken;
} catch (e) {
debugPrint('❌ Error regenerating FCM token: $e');
return null;
}
}
/// 🗑️ Delete FCM token (call on logout)
Future<bool> deleteToken() async {
try {
if (Platform.isIOS) {
final apns = await _messaging.getAPNSToken();
if (apns == null) return false;
}
// Print token before deletion
final currentToken = await _messaging.getToken();
debugPrint('📋 Current FCM Token (before delete): $currentToken');
debugPrint('🗑️ Deleting FCM token...');
await _messaging.deleteToken();
debugPrint('✅ FCM token deleted successfully');
return true;
} catch (e) {
debugPrint('❌ Error deleting FCM token: $e');
return false;
}
}
/// 📡 Listen to token refresh events
void listenToTokenRefresh(Function(String) onTokenRefreshed) {
_messaging.onTokenRefresh.listen((newToken) {
debugPrint('🔔 FCM Token auto-refreshed by Firebase: $newToken');
onTokenRefreshed(newToken);
});
}
/// 🔄 Reset and get fresh token (comprehensive reset)
Future<String?> resetAndGetFreshToken() async {
try {
// Delete existing token
await deleteToken();
// Wait longer to ensure complete reset
await Future.delayed(const Duration(seconds: 1));
// Regenerate
return await regenerateToken();
} catch (e) {
debugPrint('❌ Error in resetAndGetFreshToken: $e');
return null;
}
}
}
+262
View File
@@ -0,0 +1,262 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/view/screens/notification_screen.dart';
import 'package:taxglide/controller/api_contoller.dart';
final serviceSearchProvider = StateProvider<String>((ref) => "");
class HomePageHeader extends ConsumerWidget {
final String userName;
const HomePageHeader({super.key, required this.userName});
@override
Widget build(BuildContext context, WidgetRef ref) {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final headerHeight = screenHeight * 0.21;
final profileSize = screenWidth * 0.14;
final iconSize = screenWidth * 0.07;
final horizontalPadding = screenWidth * 0.04;
final searchBoxHeight = screenHeight * 0.07;
final welcomeFontSize = screenWidth * 0.035;
final userFontSize = screenWidth * 0.045;
final initialFontSize = screenWidth * 0.045;
// 🔴 Fetch Notification Count
final notifCountAsync = ref.watch(notificationCountProvider);
// 🔴 Fetch Profile Data for Image
final profileAsync = ref.watch(profileProvider);
return Stack(
clipBehavior: Clip.none,
children: [
// Background Header
ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
child: Image.asset(
AppAssets.homepageheader,
width: double.infinity,
height: headerHeight,
fit: BoxFit.cover,
),
),
// Profile + Welcome + Notification Icon
Positioned(
top: headerHeight * 0.15,
left: horizontalPadding,
right: horizontalPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Profile Avatar
profileAsync.when(
data: (profileModel) {
final String? logoUrl = profileModel.data?.logo;
if (logoUrl != null && logoUrl.isNotEmpty) {
return Container(
width: profileSize,
height: profileSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
image: DecorationImage(
image: NetworkImage(logoUrl),
fit: BoxFit.cover,
),
),
);
}
// Fallback to initial
return Container(
width: profileSize,
height: profileSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black.withOpacity(0.8),
border: Border.all(color: Colors.white, width: 2),
),
child: Center(
child: Text(
userName.isNotEmpty ? userName[0].toUpperCase() : "M",
style: TextStyle(
color: Colors.white,
fontSize: initialFontSize,
fontWeight: FontWeight.bold,
),
),
),
);
},
loading: () => Container(
width: profileSize,
height: profileSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[300],
border: Border.all(color: Colors.white, width: 2),
),
),
error: (e, st) => Container(
width: profileSize,
height: profileSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black.withOpacity(0.8),
border: Border.all(color: Colors.white, width: 2),
),
child: Center(
child: Text(
userName.isNotEmpty ? userName[0].toUpperCase() : "M",
style: TextStyle(
color: Colors.white,
fontSize: initialFontSize,
fontWeight: FontWeight.bold,
),
),
),
),
),
SizedBox(width: screenWidth * 0.03),
// Welcome & Username
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Welcome to TaxGlide",
style: TextStyle(
color: Colors.black,
fontSize: welcomeFontSize,
fontWeight: FontWeight.w400,
),
),
Text(
userName,
style: TextStyle(
color: Colors.black,
fontSize: userFontSize,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
// Notifications with Badge
GestureDetector(
onTap: () => Get.to(NotificationScreen()),
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.black,
),
padding: EdgeInsets.all(screenWidth * 0.025),
child: Icon(
Icons.notifications,
color: Colors.white,
size: iconSize,
),
),
// 🔴 Notification Badge
Positioned(
right: -2,
top: -2,
child: notifCountAsync.when(
loading: () => const SizedBox.shrink(),
error: (e, st) => const SizedBox.shrink(),
data: (count) {
if (count.count == 0) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Text(
count.count.toString(),
style: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
);
},
),
),
],
),
),
],
),
),
// Search Bar
Positioned(
top: headerHeight - (searchBoxHeight * 0.6),
left: screenWidth * 0.05,
right: screenWidth * 0.05,
child: Container(
height: searchBoxHeight,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(50),
border: Border.all(color: const Color(0xFFE4E4E4), width: 1),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: TextField(
textAlign: TextAlign.start, // ✅ Start (left) alignment
style: TextStyle(fontSize: screenWidth * 0.04),
onChanged: (value) {
ref.read(serviceSearchProvider.notifier).state = value;
},
decoration: InputDecoration(
hintText: "Search services...",
hintStyle: TextStyle(
color: Colors.grey,
fontSize: screenWidth * 0.04,
),
prefixIcon: Icon(
Icons.search,
color: Colors.black54,
size: screenWidth * 0.06,
),
border: InputBorder.none,
// ✅ Only vertical center
contentPadding: EdgeInsets.symmetric(
vertical: searchBoxHeight * 0.3,
),
),
),
),
),
],
);
}
}
+651
View File
@@ -0,0 +1,651 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:file_picker/file_picker.dart';
import 'package:taxglide/consts/comman_image_box.dart';
import 'package:taxglide/consts/comman_textformfileds.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:path/path.dart' as path;
/// A reusable helper class for image picking with safe permission handling.
class ImagePickerHelper {
static final ImagePicker _picker = ImagePicker();
static bool _isPicking = false;
/// Request permission depending on platform and SDK version
static Future<bool> _requestPermission(
BuildContext context,
ImageSource source,
) async {
try {
Permission permission;
String name;
if (source == ImageSource.camera) {
permission = Permission.camera;
name = 'Camera';
} else {
if (Platform.isAndroid) {
final info = await DeviceInfoPlugin().androidInfo;
if (info.version.sdkInt >= 33) {
permission = Permission.photos;
name = 'Photos';
} else {
permission = Permission.storage;
name = 'Storage';
}
} else {
permission = Permission.photos; // iOS gallery
name = 'Gallery';
}
}
var status = await permission.status;
if (status.isGranted || status.isLimited) return true;
// On iOS, if already denied, request() won't show anything.
// On Android, permanently denied also won't show anything.
if (status.isPermanentlyDenied || (Platform.isIOS && status.isDenied)) {
await _showSettingsDialog(context, name);
return false;
}
// Now request permission
status = await permission.request();
if (status.isGranted || status.isLimited) {
return true;
} else {
// If user denied it just now, or it was already denied (iOS)
await _showSettingsDialog(context, name);
return false;
}
} catch (e) {
debugPrint('Permission request failed: $e');
return Platform.isIOS;
}
}
static Future<void> _showSettingsDialog(
BuildContext context,
String name,
) async {
final openSettings =
await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: Text('$name Permission Required'),
content: Text(
'Allow Taxglide to access your $name to capture and upload files easily. You can enable this in the app settings.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Go to Settings'),
),
],
),
) ??
false;
if (openSettings) await openAppSettings();
}
/// Pick single image safely
static Future<void> pickSingleImage({
required BuildContext context,
required void Function(File?) onImageSelected,
required void Function(String?) setError,
}) async {
if (_isPicking) return;
_isPicking = true;
try {
// Ask user: Camera or Gallery
final source = await showDialog<ImageSource>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: const Text('Select Image Source'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.camera_alt, color: Colors.blue),
title: const Text('Camera'),
onTap: () => Navigator.pop(ctx, ImageSource.camera),
),
ListTile(
leading: const Icon(Icons.photo, color: Colors.blue),
title: const Text('Gallery'),
onTap: () => Navigator.pop(ctx, ImageSource.gallery),
),
],
),
),
);
if (source == null) {
_isPicking = false;
return;
}
// Permission check
final hasPermission = await _requestPermission(context, source);
if (!hasPermission) {
_isPicking = false;
return;
}
// Image selection
final pickedFile = await _picker.pickImage(source: source);
if (pickedFile != null) {
onImageSelected(File(pickedFile.path));
setError(null);
}
} catch (e) {
debugPrint('Image pick error: $e');
Get.snackbar(
"Error",
"Failed to pick image: $e",
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
_isPicking = false;
}
}
/// 🔥 NEW: Pick Image or PDF
static Future<void> pickImageOrPdf({
required BuildContext context,
required void Function(File?) onFileSelected,
required void Function(String?) setError,
}) async {
if (_isPicking) return;
_isPicking = true;
try {
// Ask user: Camera, Gallery, or PDF
final choice = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: const Text('Select File Source'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.camera_alt, color: Colors.blue),
title: const Text('Camera'),
onTap: () => Navigator.pop(ctx, 'camera'),
),
ListTile(
leading: const Icon(Icons.photo, color: Colors.blue),
title: const Text('Gallery (Image)'),
onTap: () => Navigator.pop(ctx, 'gallery'),
),
ListTile(
leading: const Icon(Icons.picture_as_pdf, color: Colors.red),
title: const Text('Select PDF'),
onTap: () => Navigator.pop(ctx, 'pdf'),
),
],
),
),
);
if (choice == null) {
_isPicking = false;
return;
}
if (choice == 'pdf') {
// Pick PDF using file_picker
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
);
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
// Check file size (max 10MB)
final fileSizeInBytes = await file.length();
final fileSizeInMB = fileSizeInBytes / (1024 * 1024);
if (fileSizeInMB > 10) {
Get.snackbar(
"Error",
"PDF file size should not exceed 10MB",
backgroundColor: Colors.red,
colorText: Colors.white,
);
_isPicking = false;
return;
}
onFileSelected(file);
setError(null);
}
} else {
// Pick Image (Camera or Gallery)
final source = choice == 'camera'
? ImageSource.camera
: ImageSource.gallery;
// Permission check
final hasPermission = await _requestPermission(context, source);
if (!hasPermission) {
_isPicking = false;
return;
}
// Image selection
final pickedFile = await _picker.pickImage(source: source);
if (pickedFile != null) {
onFileSelected(File(pickedFile.path));
setError(null);
}
}
} catch (e) {
debugPrint('File pick error: $e');
Get.snackbar(
"Error",
"Failed to pick file: $e",
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
_isPicking = false;
}
}
}
class LabeledTextField extends StatelessWidget {
final GlobalKey fieldKey;
final String label;
final String keyName;
final String hint;
final TextEditingController controller;
final FocusNode focusNode;
final TextInputType keyboardType;
final Map<String, String?> errors;
final Function(String) onChanged;
final bool readOnly;
/// ✅ ADD THESE
final List<TextInputFormatter>? inputFormatters;
final TextCapitalization textCapitalization;
const LabeledTextField({
super.key,
required this.fieldKey,
required this.label,
required this.keyName,
required this.hint,
required this.controller,
required this.focusNode,
required this.errors,
required this.onChanged,
this.readOnly = false,
this.keyboardType = TextInputType.text,
/// ✅ DEFAULT VALUES
this.inputFormatters,
this.textCapitalization = TextCapitalization.none,
});
@override
Widget build(BuildContext context) {
return Padding(
key: fieldKey,
padding: const EdgeInsets.only(bottom: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 16,
height: 1.3,
letterSpacing: 0.64,
color: Colors.black,
),
),
const SizedBox(height: 8),
/// ✅ PASS FORMATTERS & CAPITALIZATION HERE
CommanTextFormField(
controller: controller,
hintText: hint,
keyboardType: keyboardType,
focusNode: focusNode,
hasError: errors[keyName] != null,
onChanged: onChanged,
readOnly: readOnly,
inputFormatters: inputFormatters,
textCapitalization: textCapitalization,
),
if (errors[keyName] != null)
Padding(
padding: const EdgeInsets.only(top: 4, left: 6),
child: Text(
errors[keyName]!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
],
),
);
}
}
// 🔥 Updated SingleImageSectionField with URL support (Image only - for Logo)
class SingleImageSectionField extends StatelessWidget {
final GlobalKey imageKey;
final String title;
final File? imageFile;
final String? imageUrl;
final String? errorText;
final Function(String?) setError;
final Function(File?) onImageSelected;
final VoidCallback onImageRemoved;
const SingleImageSectionField({
super.key,
required this.imageKey,
required this.title,
required this.imageFile,
required this.imageUrl,
required this.errorText,
required this.setError,
required this.onImageSelected,
required this.onImageRemoved,
});
@override
Widget build(BuildContext context) {
final hasImage = imageFile != null || imageUrl != null;
return Padding(
key: imageKey,
padding: const EdgeInsets.only(bottom: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 16,
height: 1.3,
letterSpacing: 0.64,
color: Colors.black,
),
),
const SizedBox(height: 8),
CommanImageBox(
onTap: () => ImagePickerHelper.pickSingleImage(
context: context,
onImageSelected: onImageSelected,
setError: setError,
),
text: hasImage ? 'Change Image' : 'Upload Image',
),
if (errorText != null)
Padding(
padding: const EdgeInsets.only(top: 4, left: 6),
child: Text(
errorText!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
if (hasImage)
Container(
margin: const EdgeInsets.only(top: 10),
child: Stack(
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: imageFile != null
? Image.file(imageFile!, fit: BoxFit.cover)
: CachedNetworkImage(
imageUrl: imageUrl!,
fit: BoxFit.cover,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) =>
const Icon(Icons.error, color: Colors.red),
),
),
),
Positioned(
right: 0,
top: 0,
child: GestureDetector(
onTap: onImageRemoved,
child: Container(
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
),
],
),
padding: const EdgeInsets.all(4),
child: const Icon(
Icons.close,
color: Colors.white,
size: 18,
),
),
),
),
],
),
),
],
),
);
}
}
// 🔥 NEW: File Section Field (Supports both Image and PDF)
class FileUploadSectionField extends StatelessWidget {
final GlobalKey fileKey;
final String title;
final File? fileData;
final String? fileUrl;
final String? errorText;
final Function(String?) setError;
final Function(File?) onFileSelected;
final VoidCallback onFileRemoved;
const FileUploadSectionField({
super.key,
required this.fileKey,
required this.title,
required this.fileData,
required this.fileUrl,
required this.errorText,
required this.setError,
required this.onFileSelected,
required this.onFileRemoved,
});
bool _isPdfFile(String? filePath) {
if (filePath == null) return false;
return path.extension(filePath).toLowerCase() == '.pdf';
}
bool _isImageFile(String? filePath) {
if (filePath == null) return false;
final ext = path.extension(filePath).toLowerCase();
return ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].contains(ext);
}
@override
Widget build(BuildContext context) {
final hasFile = fileData != null || fileUrl != null;
final isPdf = _isPdfFile(fileData?.path ?? fileUrl);
final isImage = _isImageFile(fileData?.path ?? fileUrl);
return Padding(
key: fileKey,
padding: const EdgeInsets.only(bottom: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 16,
height: 1.3,
letterSpacing: 0.64,
color: Colors.black,
),
),
const SizedBox(height: 8),
CommanImageBox(
onTap: () => ImagePickerHelper.pickImageOrPdf(
context: context,
onFileSelected: onFileSelected,
setError: setError,
),
text: hasFile ? 'Change File' : 'Upload Image/PDF',
),
if (errorText != null)
Padding(
padding: const EdgeInsets.only(top: 4, left: 6),
child: Text(
errorText!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
// Display file preview
if (hasFile)
Container(
margin: const EdgeInsets.only(top: 10),
child: Stack(
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: isPdf
// Show PDF icon
? Container(
color: Colors.red.shade50,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.picture_as_pdf,
size: 48,
color: Colors.red.shade700,
),
const SizedBox(height: 8),
Text(
'PDF',
style: TextStyle(
color: Colors.red.shade700,
fontWeight: FontWeight.bold,
),
),
],
),
)
: isImage && fileData != null
// Show local image
? Image.file(fileData!, fit: BoxFit.cover)
: isImage && fileUrl != null
// Show network image
? CachedNetworkImage(
imageUrl: fileUrl!,
fit: BoxFit.cover,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) =>
const Icon(Icons.error, color: Colors.red),
)
// Unknown file type
: Container(
color: Colors.grey.shade100,
child: const Icon(
Icons.insert_drive_file,
size: 48,
color: Colors.grey,
),
),
),
),
Positioned(
right: 0,
top: 0,
child: GestureDetector(
onTap: onFileRemoved,
child: Container(
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
),
],
),
padding: const EdgeInsets.all(4),
child: const Icon(
Icons.close,
color: Colors.white,
size: 18,
),
),
),
),
],
),
),
],
),
);
}
}
+102
View File
@@ -0,0 +1,102 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class LocalStore {
// Secure storage instance
final _storage = const FlutterSecureStorage();
/// --------------------------------------------------------
/// 🔐 SAVE LOGIN / SIGNUP DATA
/// --------------------------------------------------------
Future<void> saveLoginData(Map<String, dynamic> data) async {
final token = data['access_token'] ?? '';
final role = data['role'] ?? '';
// Keep existing FCM token (do not overwrite)
String? existingFcmToken = await _storage.read(key: 'fcm_token');
// Save token & role
await _storage.write(key: 'access_token', value: token.toString());
await _storage.write(key: 'role', value: role.toString());
// ⭐ Save general_chat_id if available
if (data['general_chat_id'] != null) {
await _storage.write(
key: 'general_chat_id',
value: data['general_chat_id'].toString(),
);
}
// ⭐ Save USER ID if available
if (data['user'] != null && data['user']['id'] != null) {
await _storage.write(
key: 'user_id',
value: data['user']['id'].toString(),
);
}
// ⭐ If root level ID exists (not in your JSON but future-safe)
if (data['id'] != null) {
await _storage.write(key: 'main_id', value: data['id'].toString());
}
// ⭐ Save/Persist FCM token safely
if (existingFcmToken != null && existingFcmToken.isNotEmpty) {
// Keep old FCM
await _storage.write(key: 'fcm_token', value: existingFcmToken);
} else if (data['fcm_token'] != null &&
data['fcm_token'].toString().isNotEmpty) {
// Save new FCM token
await _storage.write(
key: 'fcm_token',
value: data['fcm_token'].toString(),
);
}
}
/// --------------------------------------------------------
/// 🔥 SAVE FCM TOKEN ALONE
/// --------------------------------------------------------
Future<void> saveFcmToken(String token) async {
await _storage.write(key: 'fcm_token', value: token);
}
/// --------------------------------------------------------
/// 🔍 GETTERS
/// --------------------------------------------------------
Future<String?> getToken() async {
return await _storage.read(key: 'access_token');
}
Future<String?> getRole() async {
return await _storage.read(key: 'role');
}
Future<String?> getGeneralChatId() async {
return await _storage.read(key: 'general_chat_id');
}
Future<String?> getUserId() async {
return await _storage.read(key: 'user_id');
}
Future<String?> getMainId() async {
return await _storage.read(key: 'main_id');
}
/// --------------------------------------------------------
/// 🧹 CLEAR ALL (OPTIONAL KEEP FCM TOKEN)
/// --------------------------------------------------------
Future<void> clearAll({bool keepFcm = true}) async {
if (keepFcm) {
// Preserve FCM token
String? fcmToken = await _storage.read(key: 'fcm_token');
await _storage.deleteAll();
if (fcmToken != null) {
await _storage.write(key: 'fcm_token', value: fcmToken);
}
} else {
// Full wipe
await _storage.deleteAll();
}
}
}
+328
View File
@@ -0,0 +1,328 @@
import 'dart:async';
import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:taxglide/services/notification_service.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/controller/api_contoller.dart';
class NotificationWebSocket {
static final NotificationWebSocket _instance =
NotificationWebSocket._internal();
factory NotificationWebSocket() => _instance;
NotificationWebSocket._internal();
WebSocketChannel? _channel;
StreamSubscription? _subscription;
Timer? _reconnectTimer;
bool _isConnected = false;
bool _isIntentionalClose = false;
String? _userId;
String? _wsUrl;
ProviderContainer? _container;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 10;
/// ChatId the user is currently VIEWING.
String? _activeChatId;
/// ChatId whose `chat.<id>` channel we are subscribed to on this WS.
String? _subscribedChatChannelId;
// ─────────────────────────── Public API ────────────────────────────────────
/// Call from LiveChatScreen.initState().
void setActiveChatId(String chatId) {
_activeChatId = chatId;
debugPrint('NotificationWS: viewing chat $chatId');
_subscribeToChatChannel(chatId);
}
/// Call from LiveChatScreen.dispose()
void clearActiveChatId() {
debugPrint('NotificationWS: left chat $_activeChatId');
_activeChatId = null;
}
/// Subscribe to a chat channel for updates.
void watchChatChannel(String chatId) {
debugPrint('Eye NotificationWS: watchChatChannel($chatId)');
_subscribeToChatChannel(chatId);
}
/// Connect (or skip if already connected for the same user).
Future<void> connect({
required String userId,
required ProviderContainer container,
String baseUrl = 'wss://taxglide.amrithaa.net:443/reverb',
String appKey = '2yj0lyzc9ylw2h03ts6i',
}) async {
if (_isConnected && _userId == userId) {
debugPrint('⚡ NotificationWS: already connected for user $userId');
return;
}
await disconnect();
_userId = userId;
_container = container;
_wsUrl = '$baseUrl/app/$appKey?protocol=7&client=js&version=1.0';
_isIntentionalClose = false;
_reconnectAttempts = 0;
_openSocket();
}
// ──────────────────────── Socket lifecycle ──────────────────────────────────
void _openSocket() {
try {
debugPrint('🔌 NotificationWS connecting…');
_channel = WebSocketChannel.connect(Uri.parse(_wsUrl!));
_subscription = _channel!.stream.listen(
_handleMessage,
onError: _handleError,
onDone: _handleDone,
);
_isConnected = true;
_subscribeToUserChannel();
// Re-subscribe to chat channel after reconnect if we had one before.
final chatId = _subscribedChatChannelId;
if (chatId != null) {
_sendRaw({
'event': 'pusher:subscribe',
'data': {'channel': 'chat.$chatId'},
});
debugPrint('📡 NotificationWS: re-subscribed to chat.$chatId');
}
} catch (e) {
debugPrint('❌ NotificationWS connect error: $e');
_isConnected = false;
_scheduleReconnect();
}
}
void _subscribeToUserChannel() {
if (_userId == null) return;
_sendRaw({
'event': 'pusher:subscribe',
'data': {'channel': 'user-chat-notification.$_userId'},
});
debugPrint(
'📡 NotificationWS: subscribed to user-chat-notification.$_userId',
);
}
void _subscribeToChatChannel(String chatId) {
if (_subscribedChatChannelId == chatId) return; // already subscribed
// Unsubscribe previous chat channel
if (_subscribedChatChannelId != null) {
_sendRaw({
'event': 'pusher:unsubscribe',
'data': {'channel': 'chat.$_subscribedChatChannelId'},
});
debugPrint(
'📡 NotificationWS: unsubscribed from chat.$_subscribedChatChannelId',
);
}
_subscribedChatChannelId = chatId;
_sendRaw({
'event': 'pusher:subscribe',
'data': {'channel': 'chat.$chatId'},
});
debugPrint('📡 NotificationWS: subscribed to chat.$chatId');
}
// ──────────────────────── Message handler ──────────────────────────────────
void _handleMessage(dynamic raw) {
debugPrint('🔔 NotificationWS raw: $raw');
try {
final msg = jsonDecode(raw) as Map<String, dynamic>;
final event = msg['event']?.toString() ?? '';
// ── Pusher connection established → re-subscribe ──────────────────────
if (event == 'pusher:connection_established') {
debugPrint('🔗 NotificationWS: connection established');
_subscribeToUserChannel();
final chatId = _subscribedChatChannelId;
if (chatId != null) {
_sendRaw({
'event': 'pusher:subscribe',
'data': {'channel': 'chat.$chatId'},
});
}
return;
}
// ── Skip all other Pusher internal events ─────────────────────────────
if (event.startsWith('pusher')) return;
if (event.startsWith('pusher_internal')) return;
// ── Refresh Riverpod badge count ──────────────────────────────────────
try {
_container?.read(notificationTriggerProvider.notifier).state++;
} catch (_) {}
// ── Determine which chat this message belongs to ──────────────────────
String? incomingChatId;
final channelStr = msg['channel']?.toString() ?? '';
if (channelStr.startsWith('chat.')) {
incomingChatId = channelStr.split('.').last;
}
// ── Parse the payload ─────────────────────────────────────────────────
final rawData = msg['data'];
if (rawData == null) return;
dynamic data;
try {
data = rawData is String ? jsonDecode(rawData) : rawData;
} catch (_) {
data = rawData;
}
// Flatten list → use first element
if (data is List && data.isNotEmpty) {
data = data[0];
}
if (data is! Map) {
debugPrint('⚠️ NotificationWS: unrecognised data format, skipping');
return;
}
final dataMap = Map<String, dynamic>.from(data);
debugPrint('🔔 NotificationWS data keys: ${dataMap.keys.toList()}');
// ── Skip ping-only payloads {status, reload} ──────────────────────────
// The backend first fires a lightweight notification-trigger event that
// only contains `status` and `reload`. There is nothing to display yet;
// the full message list arrives in the next event on the same channel.
final onlyStatusReload =
dataMap.containsKey('status') &&
dataMap.containsKey('reload') &&
!dataMap.containsKey('message') &&
!dataMap.containsKey('chat_by');
if (onlyStatusReload) {
debugPrint('🔔 NotificationWS: reload ping received, badge refreshed');
return;
}
// ── Skip user's own messages ──────────────────────────────────────────
final chatBy = dataMap['chat_by']?.toString().toLowerCase() ?? '';
if (chatBy == 'user' || chatBy == 'customer') {
debugPrint('🔕 NotificationWS: own message, skipping');
return;
}
// ── Extract routing info if needed for internal state ─────────────
final String? page =
dataMap['page']?.toString() ?? dataMap['type']?.toString();
final String? pageId =
dataMap['page_id']?.toString() ?? dataMap['pageId']?.toString();
if (page == null || page.isEmpty || pageId == null || pageId.isEmpty) {
// Chat Notification fallback
incomingChatId ??=
dataMap['chat_id']?.toString() ??
dataMap['chatId']?.toString() ??
(dataMap['parent_tag'] != null && dataMap['parent_tag'] is Map
? dataMap['parent_tag']['id']?.toString()
: null) ??
dataMap['id']?.toString();
if (incomingChatId != null && incomingChatId != '0') {
_subscribeToChatChannel(incomingChatId);
}
}
// ── Show Foreground Notification ─────────────────────────────────────
// Only show if it's NOT a message for the currently active chat screen.
// Pass incomingChatId as the dedup tag so that if FCM already fired a
// notification for this same chat, this one *replaces* it on Android
// instead of showing a second notification.
if (incomingChatId != _activeChatId) {
// Prefer the sender name from dataMap for the title.
final String senderTitle =
dataMap['sender_name']?.toString() ??
dataMap['name']?.toString() ??
dataMap['chat_by']?.toString() ??
'New Message';
final RemoteMessage fcmLikeMessage = RemoteMessage(
data: dataMap,
notification: RemoteNotification(
title: senderTitle,
body: dataMap['message']?.toString() ?? 'You have a new message',
),
);
Get.find<NotificationService>().showForegroundNotification(
fcmLikeMessage,
tag: incomingChatId, // dedup key – same chat collapses into 1 notif
);
}
} catch (e, st) {
debugPrint('❌ NotificationWS _handleMessage error: $e\n$st');
}
}
void _sendRaw(Map<String, dynamic> message) {
try {
_channel?.sink.add(jsonEncode(message));
} catch (e) {
debugPrint('❌ NotificationWS send error: $e');
}
}
// ──────────────────────── Reconnect ────────────────────────────────────────
void _handleError(dynamic error) {
debugPrint('❌ NotificationWS error: $error');
_isConnected = false;
_scheduleReconnect();
}
void _handleDone() {
debugPrint('🔌 NotificationWS: connection closed');
_isConnected = false;
if (!_isIntentionalClose) _scheduleReconnect();
}
void _scheduleReconnect() {
if (_isIntentionalClose || _reconnectAttempts >= _maxReconnectAttempts) {
return;
}
_reconnectAttempts++;
final delay = Duration(seconds: _reconnectAttempts * 2);
debugPrint(
'🔄 NotificationWS: reconnect #$_reconnectAttempts in ${delay.inSeconds}s',
);
_reconnectTimer = Timer(delay, () {
if (!_isIntentionalClose) _openSocket();
});
}
Future<void> disconnect() async {
_isIntentionalClose = true;
_reconnectTimer?.cancel();
await _subscription?.cancel();
await _channel?.sink.close();
_isConnected = false;
_subscribedChatChannelId = null;
debugPrint('🔌 NotificationWS: disconnected');
}
}
+110
View File
@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
/// Responsive utility class for all screens
class ResponsiveUtils {
final BuildContext context;
ResponsiveUtils(this.context);
// Get screen width
double get screenWidth => MediaQuery.of(context).size.width;
// Get screen height
double get screenHeight => MediaQuery.of(context).size.height;
// Device type checks
bool get isMobile => screenWidth < 600;
bool get isTablet => screenWidth >= 600 && screenWidth < 900;
bool get isDesktop => screenWidth >= 900;
// Responsive width percentage
double widthPercent(double percent) => screenWidth * (percent / 100);
// Responsive height percentage
double heightPercent(double percent) => screenHeight * (percent / 100);
// Responsive font size
double fontSize({required double mobile, double? tablet, double? desktop}) {
if (isDesktop && desktop != null) return desktop;
if (isTablet && tablet != null) return tablet;
return mobile;
}
// Responsive spacing
double spacing({required double mobile, double? tablet, double? desktop}) {
if (isDesktop && desktop != null) return desktop;
if (isTablet && tablet != null) return tablet;
return mobile;
}
// Responsive padding
EdgeInsets padding({
required double mobile,
double? tablet,
double? desktop,
}) {
final value = spacing(mobile: mobile, tablet: tablet, desktop: desktop);
return EdgeInsets.all(value);
}
// Responsive horizontal padding
EdgeInsets horizontalPadding({
required double mobile,
double? tablet,
double? desktop,
}) {
final value = spacing(mobile: mobile, tablet: tablet, desktop: desktop);
return EdgeInsets.symmetric(horizontal: value);
}
// Responsive vertical padding
EdgeInsets verticalPadding({
required double mobile,
double? tablet,
double? desktop,
}) {
final value = spacing(mobile: mobile, tablet: tablet, desktop: desktop);
return EdgeInsets.symmetric(vertical: value);
}
// Responsive border radius
double borderRadius({
required double mobile,
double? tablet,
double? desktop,
}) {
if (isDesktop && desktop != null) return desktop;
if (isTablet && tablet != null) return tablet;
return mobile;
}
// Card width for list items
double cardWidth({
double mobilePercent = 70,
double tabletPercent = 45,
double desktopPercent = 30,
}) {
if (isDesktop) return widthPercent(desktopPercent);
if (isTablet) return widthPercent(tabletPercent);
return widthPercent(mobilePercent);
}
// Get value based on screen size
T getValue<T>({required T mobile, T? tablet, T? desktop}) {
if (isDesktop && desktop != null) return desktop;
if (isTablet && tablet != null) return tablet;
return mobile;
}
}
/// Extension on BuildContext for easy access
extension ResponsiveExtension on BuildContext {
ResponsiveUtils get responsive => ResponsiveUtils(this);
// Quick access methods
double get screenWidth => MediaQuery.of(this).size.width;
double get screenHeight => MediaQuery.of(this).size.height;
bool get isMobile => screenWidth < 600;
bool get isTablet => screenWidth >= 600 && screenWidth < 900;
bool get isDesktop => screenWidth >= 900;
}
+105
View File
@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/app_colors.dart';
class ValidationPopup {
// Show SnackBar
void _showSnackBar(BuildContext context, String msg, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 2),
backgroundColor: isError
? Colors.red
: AppColors.commanbutton, // 🔴 Error red
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 6,
margin: const EdgeInsets.symmetric(horizontal: 70, vertical: 25),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
content: SizedBox(
width: 200,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(AppAssets.taxgildelogoauth, height: 18, width: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
msg,
style: const TextStyle(color: Colors.white, fontSize: 12),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
],
),
),
),
);
}
// Validate Mobile Number
bool validateMobileNumber(BuildContext context, String mobileNumber) {
// Check if empty
if (mobileNumber.isEmpty) {
_showSnackBar(context, "Please enter mobile number", isError: true);
return false;
}
// Remove spaces and special characters
String cleanNumber = mobileNumber.replaceAll(RegExp(r'[^\d]'), '');
// Check if contains only digits
if (!RegExp(r'^[0-9]+$').hasMatch(cleanNumber)) {
_showSnackBar(
context,
"Mobile number must contain only digits",
isError: true,
);
return false;
}
// Check length (Indian mobile numbers are 10 digits)
if (cleanNumber.length != 10) {
_showSnackBar(context, "Mobile number must be 10 digits", isError: true);
return false;
}
// Check if starts with valid digit (6-9 for Indian numbers)
if (!RegExp(r'^[6-9]').hasMatch(cleanNumber)) {
_showSnackBar(
context,
"Mobile number must start with 6, 7, 8, or 9",
isError: true,
);
return false;
}
// All validations passed
return true;
}
// Add this method to your ValidationPopup class
bool validateEmail(BuildContext context, String email) {
// Email regex pattern
final emailRegex = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (!emailRegex.hasMatch(email)) {
showErrorMessage(context, "Please enter a valid email address");
return false;
}
return true;
}
// Show success message
void showSuccessMessage(BuildContext context, String msg) {
_showSnackBar(context, msg, isError: false);
}
// Show error message
void showErrorMessage(BuildContext context, String msg) {
_showSnackBar(context, msg, isError: true);
}
}