Implemented Razorpay Payment Gateway integration - April 17, 2026, 5:56 PM

This commit is contained in:
Sarankumar 2026-04-17 17:58:26 +05:30
parent ece167ea75
commit 41d7cfa8c6
11 changed files with 826 additions and 425 deletions

View File

@ -45,6 +45,10 @@ android {
getByName("release") { getByName("release") {
// TODO: Replace with your release signing config // TODO: Replace with your release signing config
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
} }
} }
} }

5
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,5 @@
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
-keep class com.razorpay.** {*;}
-dontwarn com.razorpay.**

View File

@ -117,6 +117,12 @@ PODS:
- permission_handler_apple (9.3.0): - permission_handler_apple (9.3.0):
- Flutter - Flutter
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- razorpay-core-pod (1.0.6)
- razorpay-pod (1.5.3):
- razorpay-core-pod (= 1.0.6)
- razorpay_flutter (1.1.10):
- Flutter
- razorpay-pod
- SDWebImage (5.21.7): - SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7) - SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7) - SDWebImage/Core (5.21.7)
@ -140,6 +146,7 @@ DEPENDENCIES:
- open_filex (from `.symlinks/plugins/open_filex/ios`) - open_filex (from `.symlinks/plugins/open_filex/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- razorpay_flutter (from `.symlinks/plugins/razorpay_flutter/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@ -156,6 +163,8 @@ SPEC REPOS:
- GoogleUtilities - GoogleUtilities
- nanopb - nanopb
- PromisesObjC - PromisesObjC
- razorpay-core-pod
- razorpay-pod
- SDWebImage - SDWebImage
- SwiftyGif - SwiftyGif
@ -184,6 +193,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple: permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
razorpay_flutter:
:path: ".symlinks/plugins/razorpay_flutter/ios"
sqflite_darwin: sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin" :path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios: url_launcher_ios:
@ -213,6 +224,9 @@ SPEC CHECKSUMS:
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
razorpay-core-pod: 6da5fb4ed280279d665507fdb50e213d793a5fe2
razorpay-pod: 4385cf844aa29389313b741e20c72fe668970d49
razorpay_flutter: 0e98e4fcaae27ad50e011d85f66d85e0a008754a
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4

View File

@ -1,5 +1,6 @@
import Flutter import Flutter
import UIKit import UIKit
import Firebase
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
@ -7,6 +8,7 @@ import UIKit
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
FirebaseApp.configure()
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {

View File

@ -27,4 +27,5 @@ class ConstsApi {
"$baseUrl/api/notification/un_read_count"; "$baseUrl/api/notification/un_read_count";
static const String dashboard = "$baseUrl/api/get_dashboard_data"; static const String dashboard = "$baseUrl/api/get_dashboard_data";
static const String proformaaccept = "$baseUrl/api/proforma/accept"; static const String proformaaccept = "$baseUrl/api/proforma/accept";
static const String payNow = "$baseUrl/api/pay_now";
} }

View File

@ -861,6 +861,36 @@ debugPrint('📦 KYC Response Body: ${response.body}');
} }
} }
Future<Map<String, dynamic>> payNow({required int id}) async {
try {
final token = await _localStore.getToken();
final uri = Uri.parse(ConstsApi.payNow).replace(
queryParameters: {'id': id.toString()},
);
final response = await http.post(
uri,
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
},
);
final data = jsonDecode(response.body);
debugPrint("🚀 Pay Now API Response: $data");
print("🚀 Pay Now API Response: $data"); // Force print as well
if (response.statusCode != 200 && response.statusCode != 201) {
throw data['message'] ?? 'Failed to process payment';
}
return data;
} catch (e) {
debugPrint('❌ Pay Now API Error: $e');
rethrow;
}
}
Future<void> proformaaccept({required int proformaId}) async { Future<void> proformaaccept({required int proformaId}) async {
try { try {
final token = await _localStore.getToken(); final token = await _localStore.getToken();

View File

@ -0,0 +1,70 @@
import 'package:razorpay_flutter/razorpay_flutter.dart';
import 'package:flutter/foundation.dart';
class RazorpayService {
late Razorpay _razorpay;
final Function(PaymentSuccessResponse)? onSuccess;
final Function(PaymentFailureResponse)? onFailure;
final Function(ExternalWalletResponse)? onExternalWallet;
RazorpayService({
this.onSuccess,
this.onFailure,
this.onExternalWallet,
}) {
_razorpay = Razorpay();
_razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
_razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
_razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
}
void _handlePaymentSuccess(PaymentSuccessResponse response) {
debugPrint("✅ Razorpay Payment Success: ${response.paymentId}");
onSuccess?.call(response);
}
void _handlePaymentError(PaymentFailureResponse response) {
debugPrint("❌ Razorpay Payment Error: ${response.code} - ${response.message}");
onFailure?.call(response);
}
void _handleExternalWallet(ExternalWalletResponse response) {
debugPrint("💰 Razorpay External Wallet: ${response.walletName}");
onExternalWallet?.call(response);
}
void openCheckout({
required String key,
required dynamic amount,
required String orderId,
required String description,
String? contact,
String? email,
}) {
var options = {
'key': key,
'amount': amount,
'name': 'Taxglide',
'order_id': orderId,
'description': description,
'theme': {'color': '#61277A'},
'prefill': {
'contact': contact ?? '',
'email': email ?? '',
},
'external': {
'wallets': ['paytm']
}
};
try {
_razorpay.open(options);
} catch (e) {
debugPrint("❌ Error opening Razorpay: $e");
}
}
void dispose() {
_razorpay.clear();
}
}

View File

@ -14,6 +14,8 @@ import 'package:taxglide/model/detail_model.dart';
import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart'; import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart';
import 'package:taxglide/view/Main_controller/main_controller.dart'; import 'package:taxglide/view/Main_controller/main_controller.dart';
import 'package:taxglide/view/screens/history/completed_live_chat_screen.dart'; import 'package:taxglide/view/screens/history/completed_live_chat_screen.dart';
import 'package:taxglide/services/razorpay_service.dart';
import 'package:razorpay_flutter/razorpay_flutter.dart';
class DetailScreen extends ConsumerStatefulWidget { class DetailScreen extends ConsumerStatefulWidget {
final int id; final int id;
@ -30,13 +32,33 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
String? _downloadedInvoicePath; String? _downloadedInvoicePath;
bool _isWebSocketInitialized = false; // Add this flag bool _isWebSocketInitialized = false; // Add this flag
late RazorpayService _razorpayService;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_razorpayService = RazorpayService(
onSuccess: _handlePaymentSuccess,
onFailure: _handlePaymentFailure,
);
_initializeWebSocket(); // Initialize WebSocket _initializeWebSocket(); // Initialize WebSocket
} }
void _handlePaymentSuccess(PaymentSuccessResponse response) {
ValidationPopup().showSuccessMessage(
context,
"Payment successful! ID: ${response.paymentId}",
);
ref.invalidate(serviceDetailProvider(widget.id));
}
void _handlePaymentFailure(PaymentFailureResponse response) {
ValidationPopup().showErrorMessage(
context,
"Payment failed: ${response.message}",
);
}
// Add this method // Add this method
Future<void> _initializeWebSocket() async { Future<void> _initializeWebSocket() async {
if (_isWebSocketInitialized) return; if (_isWebSocketInitialized) return;
@ -82,6 +104,7 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
@override @override
void dispose() { void dispose() {
_razorpayService.dispose();
// Optionally disconnect WebSocket when leaving screen // Optionally disconnect WebSocket when leaving screen
// DetailsWebscokect().disconnect(); // DetailsWebscokect().disconnect();
super.dispose(); super.dispose();
@ -617,16 +640,16 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"This is your final task document", "This is your final task document",
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.042, fontSize: width * 0.042,
height: 1.4, height: 1.4,
letterSpacing: 0.03 * 16, letterSpacing: 0.03 * 16,
color: const Color(0xFF111827), color: const Color(0xFF111827),
),
textAlign: TextAlign.start,
), ),
textAlign: TextAlign.start,
),
const SizedBox(height: 15), const SizedBox(height: 15),
Row( Row(
mainAxisAlignment: mainAxisAlignment:
@ -639,21 +662,27 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
TextSpan( TextSpan(
text: 'Date: ', text: 'Date: ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontSize: width * 0.035, .copyWith(
height: 1.3, fontSize: width * 0.035,
letterSpacing: 0.04 * 13.97, height: 1.3,
color: const Color(0xFF111827), letterSpacing: 0.04 * 13.97,
), color: const Color(
0xFF111827,
),
),
), ),
TextSpan( TextSpan(
text: data.createdDate ?? "-", text: data.createdDate ?? "-",
style: AppTextStyles.regular.copyWith( style: AppTextStyles.regular
fontSize: width * 0.035, .copyWith(
height: 1.3, fontSize: width * 0.035,
letterSpacing: 0.04 * 13.97, height: 1.3,
color: const Color(0xFF111827), letterSpacing: 0.04 * 13.97,
), color: const Color(
0xFF111827,
),
),
), ),
], ],
), ),
@ -668,21 +697,27 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
TextSpan( TextSpan(
text: 'Payment : ', text: 'Payment : ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontSize: width * 0.035, .copyWith(
height: 1.3, fontSize: width * 0.035,
letterSpacing: 0.04 * 13.97, height: 1.3,
color: const Color(0xFF111827), letterSpacing: 0.04 * 13.97,
), color: const Color(
0xFF111827,
),
),
), ),
TextSpan( TextSpan(
text: data.paymentStatus ?? "-", text: data.paymentStatus ?? "-",
style: AppTextStyles.regular.copyWith( style: AppTextStyles.regular
fontSize: width * 0.035, .copyWith(
height: 1.3, fontSize: width * 0.035,
letterSpacing: 0.04 * 13.97, height: 1.3,
color: const Color(0xFF111827), letterSpacing: 0.04 * 13.97,
), color: const Color(
0xFF111827,
),
),
), ),
], ],
), ),
@ -891,9 +926,9 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
Column( Column(
children: [ children: [
if (data.paymentStatus == "Un Paid" && if (data.paymentStatus == "Un Paid" &&
!(data.serviceStatus ?? "") !(data.serviceStatus ?? "").toLowerCase().contains(
.toLowerCase() "cancelled",
.contains("cancelled")) ...[ )) ...[
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 31, vertical: 31,
@ -926,12 +961,13 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
Text( Text(
"Payment Advice", "Payment Advice",
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontSize: width * 0.042, .copyWith(
height: 1.4, fontSize: width * 0.042,
letterSpacing: 0.03 * 16, height: 1.4,
color: const Color(0xFF111827), letterSpacing: 0.03 * 16,
), color: const Color(0xFF111827),
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Row( Row(
@ -945,23 +981,35 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
TextSpan( TextSpan(
text: 'Date: ', text: 'Date: ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles
fontSize: width * 0.035, .semiBold
height: 1.3, .copyWith(
letterSpacing: 0.04 * 13.97, fontSize:
color: const Color(0xFF111827), width * 0.035,
), height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
), ),
TextSpan( TextSpan(
text: text:
data.createdDate ?? data.createdDate ??
"-", "-",
style: AppTextStyles.regular.copyWith( style: AppTextStyles
fontSize: width * 0.035, .regular
height: 1.3, .copyWith(
letterSpacing: 0.04 * 13.97, fontSize:
color: const Color(0xFF111827), width * 0.035,
), height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
), ),
], ],
), ),
@ -976,23 +1024,35 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
TextSpan( TextSpan(
text: 'Payment : ', text: 'Payment : ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles
fontSize: width * 0.035, .semiBold
height: 1.3, .copyWith(
letterSpacing: 0.04 * 13.97, fontSize:
color: const Color(0xFF111827), width * 0.035,
), height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
), ),
TextSpan( TextSpan(
text: text:
data.paymentStatus ?? data.paymentStatus ??
"-", "-",
style: AppTextStyles.regular.copyWith( style: AppTextStyles
fontSize: width * 0.035, .regular
height: 1.3, .copyWith(
letterSpacing: 0.04 * 13.97, fontSize:
color: const Color(0xFF111827), width * 0.035,
), height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
), ),
], ],
), ),
@ -1009,18 +1069,24 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
Text( Text(
"Total Amount", "Total Amount",
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontSize: width * 0.04, .copyWith(
color: const Color(0xFF111827), fontSize: width * 0.04,
), color: const Color(
0xFF111827,
),
),
), ),
Text( Text(
"${data.paymentAmount ?? '0'}", "${data.paymentAmount ?? '0'}",
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontFamily: 'Roboto', .copyWith(
fontSize: width * 0.045, fontFamily: 'Roboto',
color: const Color(0xFF111827), fontSize: width * 0.045,
), color: const Color(
0xFF111827,
),
),
), ),
], ],
), ),
@ -1029,7 +1095,37 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
CommanButton(text: 'Pay Now', onPressed: () {}), CommanButton(
text: 'Pay Now',
onPressed: () async {
try {
final response = await ApiRepository()
.payNow(
id: int.parse(data.id.toString()),
);
if (response['status'] == 'success') {
_razorpayService.openCheckout(
key: response['razorpay_key'],
amount: response['amount'],
orderId: response['order_id'],
description:
"Payment for Service ID: ${data.id}",
);
} else {
validationPopup.showErrorMessage(
context,
"Failed to initiate payment",
);
}
} catch (e) {
validationPopup.showErrorMessage(
context,
e.toString(),
);
}
},
),
], ],
), ),
), ),
@ -1066,12 +1162,13 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
Text( Text(
"Payment Advice", "Payment Advice",
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontSize: width * 0.042, .copyWith(
height: 1.4, fontSize: width * 0.042,
letterSpacing: 0.03 * 16, height: 1.4,
color: const Color(0xFF111827), letterSpacing: 0.03 * 16,
), color: const Color(0xFF111827),
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Row( Row(
@ -1085,23 +1182,35 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
TextSpan( TextSpan(
text: 'Date: ', text: 'Date: ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles
fontSize: width * 0.035, .semiBold
height: 1.3, .copyWith(
letterSpacing: 0.04 * 13.97, fontSize:
color: const Color(0xFF111827), width * 0.035,
), height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
), ),
TextSpan( TextSpan(
text: text:
data.createdDate ?? data.createdDate ??
"-", "-",
style: AppTextStyles.regular.copyWith( style: AppTextStyles
fontSize: width * 0.035, .regular
height: 1.3, .copyWith(
letterSpacing: 0.04 * 13.97, fontSize:
color: const Color(0xFF111827), width * 0.035,
), height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
), ),
], ],
), ),
@ -1116,23 +1225,35 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
TextSpan( TextSpan(
text: 'Payment: ', text: 'Payment: ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles
fontSize: width * 0.035, .semiBold
height: 1.3, .copyWith(
letterSpacing: 0.04 * 13.97, fontSize:
color: const Color(0xFF111827), width * 0.035,
), height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
), ),
TextSpan( TextSpan(
text: text:
data.paymentStatus ?? data.paymentStatus ??
"-", "-",
style: AppTextStyles.regular.copyWith( style: AppTextStyles
fontSize: width * 0.035, .regular
height: 1.3, .copyWith(
letterSpacing: 0.04 * 13.97, fontSize:
color: const Color(0xFF111827), width * 0.035,
), height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
), ),
], ],
), ),
@ -1149,18 +1270,24 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
Text( Text(
"Total Amount", "Total Amount",
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontSize: width * 0.04, .copyWith(
color: const Color(0xFF111827), fontSize: width * 0.04,
), color: const Color(
0xFF111827,
),
),
), ),
Text( Text(
"${data.paymentAmount ?? '0'}", "${data.paymentAmount ?? '0'}",
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontFamily: 'Roboto', .copyWith(
fontSize: width * 0.045, fontFamily: 'Roboto',
color: const Color(0xFF111827), fontSize: width * 0.045,
), color: const Color(
0xFF111827,
),
),
), ),
], ],
), ),
@ -1232,26 +1359,31 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
TextSpan( TextSpan(
text: 'proforma Number : ', text: 'proforma Number : ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontSize: width * 0.035, .copyWith(
height: 1.3, fontSize: width * 0.035,
letterSpacing: 0.04 * 13.97, height: 1.3,
color: const Color( letterSpacing:
0xFF111827, 0.04 * 13.97,
), color: const Color(
), 0xFF111827,
),
),
), ),
TextSpan( TextSpan(
text: data.proformaNumber ?? text:
data.proformaNumber ??
"-", "-",
style: AppTextStyles.regular.copyWith( style: AppTextStyles.regular
fontSize: width * 0.035, .copyWith(
height: 1.3, fontSize: width * 0.035,
letterSpacing: 0.04 * 13.97, height: 1.3,
color: const Color( letterSpacing:
0xFF111827, 0.04 * 13.97,
), color: const Color(
), 0xFF111827,
),
),
), ),
], ],
), ),
@ -1267,26 +1399,32 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
TextSpan( TextSpan(
text: 'proforma Status : ', text: 'proforma Status : ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontSize: width * 0.035, .copyWith(
height: 1.3, fontSize: width * 0.035,
letterSpacing: 0.04 * 13.97, height: 1.3,
color: const Color(0xFF111827), letterSpacing: 0.04 * 13.97,
), color: const Color(
0xFF111827,
),
),
), ),
TextSpan( TextSpan(
text: data.proformaStatus ?? "-", text: data.proformaStatus ?? "-",
style: AppTextStyles.regular.copyWith( style: AppTextStyles.regular
fontSize: width * 0.035, .copyWith(
height: 1.3, fontSize: width * 0.035,
letterSpacing: 0.04 * 13.97, height: 1.3,
color: const Color(0xFF111827), letterSpacing: 0.04 * 13.97,
), color: const Color(
0xFF111827,
),
),
), ),
], ],
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
RichText( RichText(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end, textAlign: TextAlign.end,
@ -1294,21 +1432,27 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
children: [ children: [
TextSpan( TextSpan(
text: 'Subject : ', text: 'Subject : ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold
fontSize: width * 0.035, .copyWith(
height: 1.3, fontSize: width * 0.035,
letterSpacing: 0.04 * 13.97, height: 1.3,
color: const Color(0xFF111827), letterSpacing: 0.04 * 13.97,
), color: const Color(
0xFF111827,
),
),
), ),
TextSpan( TextSpan(
text: data.subject ?? "-", text: data.subject ?? "-",
style: AppTextStyles.regular.copyWith( style: AppTextStyles.regular
fontSize: width * 0.035, .copyWith(
height: 1.3, fontSize: width * 0.035,
letterSpacing: 0.04 * 13.97, height: 1.3,
color: const Color(0xFF111827), letterSpacing: 0.04 * 13.97,
), color: const Color(
0xFF111827,
),
),
), ),
], ],
), ),
@ -1650,7 +1794,7 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
), ),
) )
else else
Text( Text(
"No documents uploaded", "No documents uploaded",
style: AppTextStyles.regular.copyWith( style: AppTextStyles.regular.copyWith(
color: const Color(0xFF6B7280), color: const Color(0xFF6B7280),
@ -1661,8 +1805,10 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
), ),
), ),
if (data.chatId != null && if (data.chatId != null &&
data.chatId!.isNotEmpty && data.chatId!.isNotEmpty &&
!(data.serviceStatus ?? "").toLowerCase().contains("cancelled")) !(data.serviceStatus ?? "").toLowerCase().contains(
"cancelled",
))
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 20, horizontal: 20,
@ -1758,7 +1904,9 @@ class _DetailScreenState extends ConsumerState<DetailScreen> {
), ),
), ),
SizedBox(height: 80.0 + MediaQuery.of(context).padding.bottom), SizedBox(
height: 80.0 + MediaQuery.of(context).padding.bottom,
),
], ],
), ),
); );

View File

@ -7,15 +7,60 @@ import 'package:taxglide/view/Main_controller/main_controller.dart';
import 'package:taxglide/view/screens/history/detail_screen.dart'; import 'package:taxglide/view/screens/history/detail_screen.dart';
import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart'; import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart';
import 'package:taxglide/view/screens/history/completed_live_chat_screen.dart'; import 'package:taxglide/view/screens/history/completed_live_chat_screen.dart';
import 'package:taxglide/services/razorpay_service.dart';
import 'package:razorpay_flutter/razorpay_flutter.dart';
import 'package:taxglide/consts/validation_popup.dart';
import 'package:taxglide/controller/api_repository.dart';
class PendingScreen extends ConsumerWidget { class PendingScreen extends ConsumerStatefulWidget {
final String status; final String status;
const PendingScreen({super.key, required this.status}); const PendingScreen({super.key, required this.status});
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<PendingScreen> createState() => _PendingScreenState();
final pendingAsync = ref.watch(serviceHistoryNotifierProvider(status)); }
class _PendingScreenState extends ConsumerState<PendingScreen> {
late RazorpayService _razorpayService;
@override
void initState() {
super.initState();
_razorpayService = RazorpayService(
onSuccess: _handlePaymentSuccess,
onFailure: _handlePaymentFailure,
);
}
void _handlePaymentSuccess(PaymentSuccessResponse response) {
ValidationPopup().showSuccessMessage(
context,
"Payment successful! ID: ${response.paymentId}",
);
ref
.read(serviceHistoryNotifierProvider(widget.status).notifier)
.fetchServiceHistory();
}
void _handlePaymentFailure(PaymentFailureResponse response) {
ValidationPopup().showErrorMessage(
context,
"Payment failed: ${response.message}",
);
}
@override
void dispose() {
_razorpayService.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final pendingAsync = ref.watch(
serviceHistoryNotifierProvider(widget.status),
);
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final width = size.width; final width = size.width;
final height = size.height; final height = size.height;
@ -23,9 +68,11 @@ class PendingScreen extends ConsumerWidget {
// 🔄 Silent refresh when notification trigger changes // 🔄 Silent refresh when notification trigger changes
ref.listen(notificationTriggerProvider, (previous, next) { ref.listen(notificationTriggerProvider, (previous, next) {
if (previous != null && next != previous) { if (previous != null && next != previous) {
debugPrint("🔔 Silent refresh triggered for PendingScreen ($status)"); debugPrint(
"🔔 Silent refresh triggered for PendingScreen (${widget.status})",
);
ref ref
.read(serviceHistoryNotifierProvider(status).notifier) .read(serviceHistoryNotifierProvider(widget.status).notifier)
.fetchServiceHistory(isSilent: true); .fetchServiceHistory(isSilent: true);
ref.invalidate(countProvider); ref.invalidate(countProvider);
} }
@ -36,7 +83,9 @@ class PendingScreen extends ConsumerWidget {
final list = data.data ?? []; final list = data.data ?? [];
if (list.isEmpty) { if (list.isEmpty) {
// Using Get's capitalize extension explicitly // Using Get's capitalize extension explicitly
return Center(child: Text("No ${status.capitalizeFirst} Requests")); return Center(
child: Text("No ${widget.status.capitalizeFirst} Requests"),
);
} }
return ListView.separated( return ListView.separated(
@ -72,236 +121,268 @@ class PendingScreen extends ConsumerWidget {
children: [ children: [
// 🔹 File ID Row // 🔹 File ID Row
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
/// 🔹 File ID /// 🔹 File ID
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: 'File ID : ', text: 'File ID : ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.04, fontSize: width * 0.04,
color: const Color(0xFF111827), color: const Color(0xFF111827),
), ),
), ),
TextSpan( TextSpan(
text: '${item.id}', text: '${item.id}',
style: AppTextStyles.medium.copyWith( style: AppTextStyles.medium.copyWith(
fontSize: width * 0.04, fontSize: width * 0.04,
color: const Color(0xFF111827), color: const Color(0xFF111827),
), ),
), ),
], ],
),
),
/// 🔥 Status + Chat Icon
Row(
children: [
/// 🔴 Waiting / Pending / Cancelled
if (item.status == 'Waiting for Admin' ||
item.status == 'Payment Pending' ||
item.status == 'Cancelled By Admin')
Container(
width: width * 0.4,
height: height * 0.04,
decoration: BoxDecoration(
color: const Color(0xFFFFE8E8),
borderRadius: BorderRadius.circular(5.52),
border: Border.all(
color: const Color(0xFFFFD7D7),
width: 1.84,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.17,
offset: const Offset(0, 3.68),
),
],
),
child: Center(
child: Text(
item.status.toString(),
style: AppTextStyles.regular.copyWith(
fontSize: 11.03,
color: const Color(0xFFFF0F0F),
),
),
),
),
/// 🟢 In Progress
if (item.status == 'In Progress')
Container(
width: 98,
height: 34.9,
decoration: BoxDecoration(
color: const Color(0xFFEAFAE6),
borderRadius: BorderRadius.circular(6.21),
border: Border.all(
color: const Color(0xFFDDFFDD),
width: 2.07,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 8.08,
offset: const Offset(0, 4.14),
),
],
),
child: Center(
child: Text(
'In Progress',
style: AppTextStyles.regular.copyWith(
fontSize: 12.43,
color: const Color(0xFF12800C),
),
),
),
),
/// 🟡 Completed
if (item.status == 'Completed')
Container(
width: 87,
height: 31,
decoration: BoxDecoration(
color: const Color(0xFFFAF7E6),
borderRadius: BorderRadius.circular(5.52),
border: Border.all(
color: const Color(0xFFFFE9DD),
width: 1.84,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.17,
offset: const Offset(0, 3.68),
),
],
),
child: Center(
child: Text(
'Completed',
style: AppTextStyles.regular.copyWith(
fontSize: 11.03,
color: const Color(0xFFFF630F),
),
),
),
),
/// 🔥 Space between status & icon
const SizedBox(width: 8),
if ((item.status == 'Completed' ||
item.status == 'Cancelled By Admin' ||
item.status == 'In Progress') &&
item.chatId != null &&
item.chatId.toString().isNotEmpty &&
item.chatId.toString() != "0") ...[
/// 💬 Chat Icon with Badge
Builder(builder: (context) {
final chatIdStr = item.chatId.toString();
final countAsync = ref.watch(countProvider(chatIdStr));
return GestureDetector(
onTap: () {
final chatid = int.tryParse(chatIdStr) ?? 0;
if (chatid == 0) return;
if (item.status == 'In Progress') {
Get.to(() => LiveChatScreen(
fileid: item.id.toString(),
chatid: chatid,
))?.then((_) {
ref.read(notificationTriggerProvider.notifier).state++;
ref.invalidate(chatMessagesProvider(chatid));
ref.invalidate(countProvider(chatIdStr));
});
} else {
Get.to(() => CompletedLiveChatScreen(
fileid: item.id.toString(),
chatid: chatid,
))?.then((_) {
ref.read(notificationTriggerProvider.notifier).state++;
ref.invalidate(chatMessagesProvider(chatid));
ref.invalidate(countProvider(chatIdStr));
});
}
},
child: Stack(
clipBehavior: Clip.none,
children: [
/// 🔵 Chat Icon
Container(
width: 39,
height: 39,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF61277A),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 8.08,
offset: const Offset(0, 4.14),
),
],
),
child: const Center(
child: Icon(
Icons.message,
color: Colors.white,
size: 20,
),
),
),
/// 🔴 Badge
countAsync.when(
data: (countData) => countData.count > 0
? Positioned(
top: -4,
right: -4,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
'${countData.count}',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
), ),
), ),
),
)
: const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
),
],
),
);
}),
], /// 🔥 Status + Chat Icon
] Row(
), children: [
], /// 🔴 Waiting / Pending / Cancelled
), if (item.status == 'Waiting for Admin' ||
item.status == 'Payment Pending' ||
item.status == 'Cancelled By Admin')
Container(
width: width * 0.4,
height: height * 0.04,
decoration: BoxDecoration(
color: const Color(0xFFFFE8E8),
borderRadius: BorderRadius.circular(5.52),
border: Border.all(
color: const Color(0xFFFFD7D7),
width: 1.84,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.17,
offset: const Offset(0, 3.68),
),
],
),
child: Center(
child: Text(
item.status.toString(),
style: AppTextStyles.regular.copyWith(
fontSize: 11.03,
color: const Color(0xFFFF0F0F),
),
),
),
),
/// 🟢 In Progress
if (item.status == 'In Progress')
Container(
width: 98,
height: 34.9,
decoration: BoxDecoration(
color: const Color(0xFFEAFAE6),
borderRadius: BorderRadius.circular(6.21),
border: Border.all(
color: const Color(0xFFDDFFDD),
width: 2.07,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 8.08,
offset: const Offset(0, 4.14),
),
],
),
child: Center(
child: Text(
'In Progress',
style: AppTextStyles.regular.copyWith(
fontSize: 12.43,
color: const Color(0xFF12800C),
),
),
),
),
/// 🟡 Completed
if (item.status == 'Completed')
Container(
width: 87,
height: 31,
decoration: BoxDecoration(
color: const Color(0xFFFAF7E6),
borderRadius: BorderRadius.circular(5.52),
border: Border.all(
color: const Color(0xFFFFE9DD),
width: 1.84,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.17,
offset: const Offset(0, 3.68),
),
],
),
child: Center(
child: Text(
'Completed',
style: AppTextStyles.regular.copyWith(
fontSize: 11.03,
color: const Color(0xFFFF630F),
),
),
),
),
/// 🔥 Space between status & icon
const SizedBox(width: 8),
if ((item.status == 'Completed' ||
item.status == 'Cancelled By Admin' ||
item.status == 'In Progress') &&
item.chatId != null &&
item.chatId.toString().isNotEmpty &&
item.chatId.toString() != "0") ...[
/// 💬 Chat Icon with Badge
Builder(
builder: (context) {
final chatIdStr = item.chatId.toString();
final countAsync = ref.watch(
countProvider(chatIdStr),
);
return GestureDetector(
onTap: () {
final chatid = int.tryParse(chatIdStr) ?? 0;
if (chatid == 0) return;
if (item.status == 'In Progress') {
Get.to(
() => LiveChatScreen(
fileid: item.id.toString(),
chatid: chatid,
),
)?.then((_) {
ref
.read(
notificationTriggerProvider
.notifier,
)
.state++;
ref.invalidate(
chatMessagesProvider(chatid),
);
ref.invalidate(
countProvider(chatIdStr),
);
});
} else {
Get.to(
() => CompletedLiveChatScreen(
fileid: item.id.toString(),
chatid: chatid,
),
)?.then((_) {
ref
.read(
notificationTriggerProvider
.notifier,
)
.state++;
ref.invalidate(
chatMessagesProvider(chatid),
);
ref.invalidate(
countProvider(chatIdStr),
);
});
}
},
child: Stack(
clipBehavior: Clip.none,
children: [
/// 🔵 Chat Icon
Container(
width: 39,
height: 39,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF61277A),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(
0.25,
),
blurRadius: 8.08,
offset: const Offset(0, 4.14),
),
],
),
child: const Center(
child: Icon(
Icons.message,
color: Colors.white,
size: 20,
),
),
),
/// 🔴 Badge
countAsync.when(
data: (countData) => countData.count > 0
? Positioned(
top: -4,
right: -4,
child: Container(
padding: const EdgeInsets.all(
4,
),
decoration:
const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints:
const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
'${countData.count}',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight:
FontWeight.bold,
),
),
),
)
: const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
error: (_, __) =>
const SizedBox.shrink(),
),
],
),
);
},
),
],
],
),
],
),
SizedBox(height: height * 0.015), SizedBox(height: height * 0.015),
@ -311,10 +392,10 @@ class PendingScreen extends ConsumerWidget {
children: [ children: [
TextSpan( TextSpan(
text: 'Request Type : ', text: 'Request Type : ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.035, fontSize: width * 0.035,
color: const Color(0xFF111827), color: const Color(0xFF111827),
), ),
), ),
TextSpan( TextSpan(
text: item.service, text: item.service,
@ -337,17 +418,17 @@ class PendingScreen extends ConsumerWidget {
children: [ children: [
TextSpan( TextSpan(
text: 'Date : ', text: 'Date : ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.035, fontSize: width * 0.035,
color: const Color(0xFF111827), color: const Color(0xFF111827),
), ),
), ),
TextSpan( TextSpan(
text: item.createdDate, text: item.createdDate,
style: AppTextStyles.medium.copyWith( style: AppTextStyles.medium.copyWith(
fontSize: width * 0.035, fontSize: width * 0.035,
color: const Color(0xFF111827), color: const Color(0xFF111827),
), ),
), ),
], ],
), ),
@ -358,17 +439,17 @@ class PendingScreen extends ConsumerWidget {
children: [ children: [
TextSpan( TextSpan(
text: 'Time : ', text: 'Time : ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.035, fontSize: width * 0.035,
color: const Color(0xFF111827), color: const Color(0xFF111827),
), ),
), ),
TextSpan( TextSpan(
text: item.createdTime, text: item.createdTime,
style: AppTextStyles.medium.copyWith( style: AppTextStyles.medium.copyWith(
fontSize: width * 0.035, fontSize: width * 0.035,
color: const Color(0xFF111827), color: const Color(0xFF111827),
), ),
), ),
], ],
), ),
@ -384,10 +465,10 @@ class PendingScreen extends ConsumerWidget {
children: [ children: [
TextSpan( TextSpan(
text: 'Message : ', text: 'Message : ',
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.035, fontSize: width * 0.035,
color: const Color(0xFF111827), color: const Color(0xFF111827),
), ),
), ),
TextSpan( TextSpan(
text: item.message, text: item.message,
@ -432,8 +513,37 @@ class PendingScreen extends ConsumerWidget {
children: [ children: [
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () async {
// Add payment functionality here try {
final response = await ref
.read(apiRepositoryProvider)
.payNow(
id: int.parse(item.id.toString()),
);
if (response['status'] == 'success') {
_razorpayService.openCheckout(
key: response['razorpay_key'],
amount: response['amount'],
orderId: response['order_id'],
description:
"Payment for Service ID: ${item.id}",
);
} else {
ValidationPopup().showErrorMessage(
context,
"Failed to initiate payment",
);
}
} catch (e) {
// Error handled in repository/notifier or via snackbar
Get.snackbar(
'Error',
e.toString(),
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}, },
child: Container( child: Container(
height: height * 0.055, height: height * 0.055,
@ -452,7 +562,7 @@ class PendingScreen extends ConsumerWidget {
), ),
], ],
), ),
child: Center( child: Center(
child: Text( child: Text(
"Pay Now", "Pay Now",
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold.copyWith(
@ -492,7 +602,7 @@ class PendingScreen extends ConsumerWidget {
), ),
], ],
), ),
child: Center( child: Center(
child: Text( child: Text(
"View Details", "View Details",
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold.copyWith(
@ -525,7 +635,7 @@ class PendingScreen extends ConsumerWidget {
], ],
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
"Payment Status: Paid", "Payment Status: Paid",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: AppTextStyles.regular.copyWith( style: AppTextStyles.regular.copyWith(
@ -563,7 +673,7 @@ class PendingScreen extends ConsumerWidget {
), ),
], ],
), ),
child: Center( child: Center(
child: Text( child: Text(
"View Details", "View Details",
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold.copyWith(

View File

@ -257,6 +257,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.7" version: "2.0.7"
eventify:
dependency: transitive
description:
name: eventify
sha256: b829429f08586cc2001c628e7499e3e3c2493a1d895fd73b00ecb23351aa5a66
url: "https://pub.dev"
source: hosted
version: "1.0.1"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -944,6 +952,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
razorpay_flutter:
dependency: "direct main"
description:
name: razorpay_flutter
sha256: "2057bfaa769813f58bdc23b9cdc4d5ea493e92154f216f1d2edac507d0936b31"
url: "https://pub.dev"
source: hosted
version: "1.4.4"
redux: redux:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -41,6 +41,7 @@ dependencies:
flutter_local_notifications: ^18.0.1 flutter_local_notifications: ^18.0.1
curved_labeled_navigation_bar: ^2.0.6 curved_labeled_navigation_bar: ^2.0.6
http_parser: ^4.1.2 http_parser: ^4.1.2
razorpay_flutter: ^1.4.4