diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 271e82c..5219baa 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -45,6 +45,10 @@ android { getByName("release") { // TODO: Replace with your release signing config signingConfig = signingConfigs.getByName("debug") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..d6b5e15 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,5 @@ +-keepclassmembers class * { + @android.webkit.JavascriptInterface ; +} +-keep class com.razorpay.** {*;} +-dontwarn com.razorpay.** diff --git a/ios/Podfile.lock b/ios/Podfile.lock index df776d7..8be5712 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -117,6 +117,12 @@ PODS: - permission_handler_apple (9.3.0): - Flutter - 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/Core (= 5.21.7) - SDWebImage/Core (5.21.7) @@ -140,6 +146,7 @@ DEPENDENCIES: - open_filex (from `.symlinks/plugins/open_filex/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - 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`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -156,6 +163,8 @@ SPEC REPOS: - GoogleUtilities - nanopb - PromisesObjC + - razorpay-core-pod + - razorpay-pod - SDWebImage - SwiftyGif @@ -184,6 +193,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + razorpay_flutter: + :path: ".symlinks/plugins/razorpay_flutter/ios" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" url_launcher_ios: @@ -213,6 +224,9 @@ SPEC CHECKSUMS: path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + razorpay-core-pod: 6da5fb4ed280279d665507fdb50e213d793a5fe2 + razorpay-pod: 4385cf844aa29389313b741e20c72fe668970d49 + razorpay_flutter: 0e98e4fcaae27ad50e011d85f66d85e0a008754a SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 13473f3..805ecb8 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ import Flutter import UIKit +import Firebase @main @objc class AppDelegate: FlutterAppDelegate { @@ -7,6 +8,7 @@ import UIKit _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + FirebaseApp.configure() GeneratedPluginRegistrant.register(with: self) if #available(iOS 10.0, *) { diff --git a/lib/controller/api_consts.dart b/lib/controller/api_consts.dart index c1ded75..b797dce 100644 --- a/lib/controller/api_consts.dart +++ b/lib/controller/api_consts.dart @@ -27,4 +27,5 @@ class ConstsApi { "$baseUrl/api/notification/un_read_count"; static const String dashboard = "$baseUrl/api/get_dashboard_data"; static const String proformaaccept = "$baseUrl/api/proforma/accept"; + static const String payNow = "$baseUrl/api/pay_now"; } diff --git a/lib/controller/api_repository.dart b/lib/controller/api_repository.dart index 370c32d..4acbfe9 100644 --- a/lib/controller/api_repository.dart +++ b/lib/controller/api_repository.dart @@ -861,6 +861,36 @@ debugPrint('📦 KYC Response Body: ${response.body}'); } } + Future> 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 proformaaccept({required int proformaId}) async { try { final token = await _localStore.getToken(); diff --git a/lib/services/razorpay_service.dart b/lib/services/razorpay_service.dart new file mode 100644 index 0000000..df71d46 --- /dev/null +++ b/lib/services/razorpay_service.dart @@ -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(); + } +} diff --git a/lib/view/screens/history/detail_screen.dart b/lib/view/screens/history/detail_screen.dart index 7ae8c2d..ab43f59 100644 --- a/lib/view/screens/history/detail_screen.dart +++ b/lib/view/screens/history/detail_screen.dart @@ -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/Main_controller/main_controller.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 { final int id; @@ -30,13 +32,33 @@ class _DetailScreenState extends ConsumerState { String? _downloadedInvoicePath; bool _isWebSocketInitialized = false; // Add this flag + late RazorpayService _razorpayService; @override void initState() { super.initState(); + _razorpayService = RazorpayService( + onSuccess: _handlePaymentSuccess, + onFailure: _handlePaymentFailure, + ); _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 Future _initializeWebSocket() async { if (_isWebSocketInitialized) return; @@ -82,6 +104,7 @@ class _DetailScreenState extends ConsumerState { @override void dispose() { + _razorpayService.dispose(); // Optionally disconnect WebSocket when leaving screen // DetailsWebscokect().disconnect(); super.dispose(); @@ -337,7 +360,7 @@ class _DetailScreenState extends ConsumerState { if (previous != null && next != previous) { debugPrint("🔔 Silent refresh triggered for DetailScreen"); ref.refresh(serviceDetailProvider(widget.id)); - + // Also refresh unread count if available detailAsync.whenData((model) { final chatId = model.data?.chatId; @@ -617,16 +640,16 @@ class _DetailScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "This is your final task document", - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.042, - height: 1.4, - letterSpacing: 0.03 * 16, - color: const Color(0xFF111827), + Text( + "This is your final task document", + style: AppTextStyles.semiBold.copyWith( + fontSize: width * 0.042, + height: 1.4, + letterSpacing: 0.03 * 16, + color: const Color(0xFF111827), + ), + textAlign: TextAlign.start, ), - textAlign: TextAlign.start, - ), const SizedBox(height: 15), Row( mainAxisAlignment: @@ -639,21 +662,27 @@ class _DetailScreenState extends ConsumerState { children: [ TextSpan( text: 'Date: ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold + .copyWith( + fontSize: width * 0.035, + height: 1.3, + letterSpacing: 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), TextSpan( text: data.createdDate ?? "-", - style: AppTextStyles.regular.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles.regular + .copyWith( + fontSize: width * 0.035, + height: 1.3, + letterSpacing: 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), ], ), @@ -668,21 +697,27 @@ class _DetailScreenState extends ConsumerState { children: [ TextSpan( text: 'Payment : ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold + .copyWith( + fontSize: width * 0.035, + height: 1.3, + letterSpacing: 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), TextSpan( text: data.paymentStatus ?? "-", - style: AppTextStyles.regular.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles.regular + .copyWith( + fontSize: width * 0.035, + height: 1.3, + letterSpacing: 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), ], ), @@ -891,9 +926,9 @@ class _DetailScreenState extends ConsumerState { Column( children: [ if (data.paymentStatus == "Un Paid" && - !(data.serviceStatus ?? "") - .toLowerCase() - .contains("cancelled")) ...[ + !(data.serviceStatus ?? "").toLowerCase().contains( + "cancelled", + )) ...[ Padding( padding: const EdgeInsets.symmetric( vertical: 31, @@ -926,12 +961,13 @@ class _DetailScreenState extends ConsumerState { children: [ Text( "Payment Advice", - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.042, - height: 1.4, - letterSpacing: 0.03 * 16, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold + .copyWith( + fontSize: width * 0.042, + height: 1.4, + letterSpacing: 0.03 * 16, + color: const Color(0xFF111827), + ), ), const SizedBox(height: 20), Row( @@ -945,23 +981,35 @@ class _DetailScreenState extends ConsumerState { children: [ TextSpan( text: 'Date: ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles + .semiBold + .copyWith( + fontSize: + width * 0.035, + height: 1.3, + letterSpacing: + 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), TextSpan( text: data.createdDate ?? "-", - style: AppTextStyles.regular.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles + .regular + .copyWith( + fontSize: + width * 0.035, + height: 1.3, + letterSpacing: + 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), ], ), @@ -976,23 +1024,35 @@ class _DetailScreenState extends ConsumerState { children: [ TextSpan( text: 'Payment : ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles + .semiBold + .copyWith( + fontSize: + width * 0.035, + height: 1.3, + letterSpacing: + 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), TextSpan( text: data.paymentStatus ?? "-", - style: AppTextStyles.regular.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles + .regular + .copyWith( + fontSize: + width * 0.035, + height: 1.3, + letterSpacing: + 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), ], ), @@ -1009,18 +1069,24 @@ class _DetailScreenState extends ConsumerState { children: [ Text( "Total Amount", - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.04, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold + .copyWith( + fontSize: width * 0.04, + color: const Color( + 0xFF111827, + ), + ), ), Text( "₹ ${data.paymentAmount ?? '0'}", - style: AppTextStyles.semiBold.copyWith( - fontFamily: 'Roboto', - fontSize: width * 0.045, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold + .copyWith( + fontFamily: 'Roboto', + fontSize: width * 0.045, + color: const Color( + 0xFF111827, + ), + ), ), ], ), @@ -1029,7 +1095,37 @@ class _DetailScreenState extends ConsumerState { ), ), 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 { children: [ Text( "Payment Advice", - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.042, - height: 1.4, - letterSpacing: 0.03 * 16, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold + .copyWith( + fontSize: width * 0.042, + height: 1.4, + letterSpacing: 0.03 * 16, + color: const Color(0xFF111827), + ), ), const SizedBox(height: 20), Row( @@ -1085,23 +1182,35 @@ class _DetailScreenState extends ConsumerState { children: [ TextSpan( text: 'Date: ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles + .semiBold + .copyWith( + fontSize: + width * 0.035, + height: 1.3, + letterSpacing: + 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), TextSpan( text: data.createdDate ?? "-", - style: AppTextStyles.regular.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles + .regular + .copyWith( + fontSize: + width * 0.035, + height: 1.3, + letterSpacing: + 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), ], ), @@ -1116,23 +1225,35 @@ class _DetailScreenState extends ConsumerState { children: [ TextSpan( text: 'Payment: ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles + .semiBold + .copyWith( + fontSize: + width * 0.035, + height: 1.3, + letterSpacing: + 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), TextSpan( text: data.paymentStatus ?? "-", - style: AppTextStyles.regular.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles + .regular + .copyWith( + fontSize: + width * 0.035, + height: 1.3, + letterSpacing: + 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), ], ), @@ -1149,18 +1270,24 @@ class _DetailScreenState extends ConsumerState { children: [ Text( "Total Amount", - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.04, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold + .copyWith( + fontSize: width * 0.04, + color: const Color( + 0xFF111827, + ), + ), ), Text( "₹ ${data.paymentAmount ?? '0'}", - style: AppTextStyles.semiBold.copyWith( - fontFamily: 'Roboto', - fontSize: width * 0.045, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold + .copyWith( + fontFamily: 'Roboto', + fontSize: width * 0.045, + color: const Color( + 0xFF111827, + ), + ), ), ], ), @@ -1232,26 +1359,31 @@ class _DetailScreenState extends ConsumerState { children: [ TextSpan( text: 'proforma Number : ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color( - 0xFF111827, - ), - ), + style: AppTextStyles.semiBold + .copyWith( + fontSize: width * 0.035, + height: 1.3, + letterSpacing: + 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), TextSpan( - text: data.proformaNumber ?? + text: + data.proformaNumber ?? "-", - style: AppTextStyles.regular.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color( - 0xFF111827, - ), - ), + style: AppTextStyles.regular + .copyWith( + fontSize: width * 0.035, + height: 1.3, + letterSpacing: + 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), ], ), @@ -1267,26 +1399,32 @@ class _DetailScreenState extends ConsumerState { children: [ TextSpan( text: 'proforma Status : ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold + .copyWith( + fontSize: width * 0.035, + height: 1.3, + letterSpacing: 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), TextSpan( text: data.proformaStatus ?? "-", - style: AppTextStyles.regular.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles.regular + .copyWith( + fontSize: width * 0.035, + height: 1.3, + letterSpacing: 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), ], ), ), - const SizedBox(height: 20), + const SizedBox(height: 20), RichText( overflow: TextOverflow.ellipsis, textAlign: TextAlign.end, @@ -1294,21 +1432,27 @@ class _DetailScreenState extends ConsumerState { children: [ TextSpan( text: 'Subject : ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold + .copyWith( + fontSize: width * 0.035, + height: 1.3, + letterSpacing: 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), TextSpan( text: data.subject ?? "-", - style: AppTextStyles.regular.copyWith( - fontSize: width * 0.035, - height: 1.3, - letterSpacing: 0.04 * 13.97, - color: const Color(0xFF111827), - ), + style: AppTextStyles.regular + .copyWith( + fontSize: width * 0.035, + height: 1.3, + letterSpacing: 0.04 * 13.97, + color: const Color( + 0xFF111827, + ), + ), ), ], ), @@ -1650,7 +1794,7 @@ class _DetailScreenState extends ConsumerState { ), ) else - Text( + Text( "No documents uploaded", style: AppTextStyles.regular.copyWith( color: const Color(0xFF6B7280), @@ -1661,8 +1805,10 @@ class _DetailScreenState extends ConsumerState { ), ), if (data.chatId != null && - data.chatId!.isNotEmpty && - !(data.serviceStatus ?? "").toLowerCase().contains("cancelled")) + data.chatId!.isNotEmpty && + !(data.serviceStatus ?? "").toLowerCase().contains( + "cancelled", + )) Padding( padding: const EdgeInsets.symmetric( horizontal: 20, @@ -1758,7 +1904,9 @@ class _DetailScreenState extends ConsumerState { ), ), - SizedBox(height: 80.0 + MediaQuery.of(context).padding.bottom), + SizedBox( + height: 80.0 + MediaQuery.of(context).padding.bottom, + ), ], ), ); diff --git a/lib/view/screens/history/pending_screen.dart b/lib/view/screens/history/pending_screen.dart index b883185..d6d5511 100644 --- a/lib/view/screens/history/pending_screen.dart +++ b/lib/view/screens/history/pending_screen.dart @@ -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/Mahi_chat/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; const PendingScreen({super.key, required this.status}); @override - Widget build(BuildContext context, WidgetRef ref) { - final pendingAsync = ref.watch(serviceHistoryNotifierProvider(status)); + ConsumerState createState() => _PendingScreenState(); +} + +class _PendingScreenState extends ConsumerState { + 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 width = size.width; final height = size.height; @@ -23,9 +68,11 @@ class PendingScreen extends ConsumerWidget { // 🔄 Silent refresh when notification trigger changes ref.listen(notificationTriggerProvider, (previous, next) { if (previous != null && next != previous) { - debugPrint("🔔 Silent refresh triggered for PendingScreen ($status)"); + debugPrint( + "🔔 Silent refresh triggered for PendingScreen (${widget.status})", + ); ref - .read(serviceHistoryNotifierProvider(status).notifier) + .read(serviceHistoryNotifierProvider(widget.status).notifier) .fetchServiceHistory(isSilent: true); ref.invalidate(countProvider); } @@ -36,7 +83,9 @@ class PendingScreen extends ConsumerWidget { final list = data.data ?? []; if (list.isEmpty) { // 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( @@ -72,236 +121,268 @@ class PendingScreen extends ConsumerWidget { children: [ // 🔹 File ID Row Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - /// 🔹 File ID - RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'File ID : ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.04, - color: const Color(0xFF111827), - ), - ), - TextSpan( - text: '${item.id}', - style: AppTextStyles.medium.copyWith( - fontSize: width * 0.04, - 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, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + /// 🔹 File ID + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'File ID : ', + style: AppTextStyles.semiBold.copyWith( + fontSize: width * 0.04, + color: const Color(0xFF111827), + ), + ), + TextSpan( + text: '${item.id}', + style: AppTextStyles.medium.copyWith( + fontSize: width * 0.04, + color: const Color(0xFF111827), + ), + ), + ], ), ), - ), - ) - : 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), @@ -311,10 +392,10 @@ class PendingScreen extends ConsumerWidget { children: [ TextSpan( text: 'Request Type : ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold.copyWith( + fontSize: width * 0.035, + color: const Color(0xFF111827), + ), ), TextSpan( text: item.service, @@ -337,17 +418,17 @@ class PendingScreen extends ConsumerWidget { children: [ TextSpan( text: 'Date : ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold.copyWith( + fontSize: width * 0.035, + color: const Color(0xFF111827), + ), ), TextSpan( text: item.createdDate, - style: AppTextStyles.medium.copyWith( - fontSize: width * 0.035, - color: const Color(0xFF111827), - ), + style: AppTextStyles.medium.copyWith( + fontSize: width * 0.035, + color: const Color(0xFF111827), + ), ), ], ), @@ -358,17 +439,17 @@ class PendingScreen extends ConsumerWidget { children: [ TextSpan( text: 'Time : ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold.copyWith( + fontSize: width * 0.035, + color: const Color(0xFF111827), + ), ), TextSpan( text: item.createdTime, - style: AppTextStyles.medium.copyWith( - fontSize: width * 0.035, - color: const Color(0xFF111827), - ), + style: AppTextStyles.medium.copyWith( + fontSize: width * 0.035, + color: const Color(0xFF111827), + ), ), ], ), @@ -384,10 +465,10 @@ class PendingScreen extends ConsumerWidget { children: [ TextSpan( text: 'Message : ', - style: AppTextStyles.semiBold.copyWith( - fontSize: width * 0.035, - color: const Color(0xFF111827), - ), + style: AppTextStyles.semiBold.copyWith( + fontSize: width * 0.035, + color: const Color(0xFF111827), + ), ), TextSpan( text: item.message, @@ -432,8 +513,37 @@ class PendingScreen extends ConsumerWidget { children: [ Expanded( child: GestureDetector( - onTap: () { - // Add payment functionality here + onTap: () async { + 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( height: height * 0.055, @@ -452,7 +562,7 @@ class PendingScreen extends ConsumerWidget { ), ], ), - child: Center( + child: Center( child: Text( "Pay Now", style: AppTextStyles.semiBold.copyWith( @@ -492,7 +602,7 @@ class PendingScreen extends ConsumerWidget { ), ], ), - child: Center( + child: Center( child: Text( "View Details", style: AppTextStyles.semiBold.copyWith( @@ -525,7 +635,7 @@ class PendingScreen extends ConsumerWidget { ], ), alignment: Alignment.center, - child: Text( + child: Text( "Payment Status: Paid", textAlign: TextAlign.center, style: AppTextStyles.regular.copyWith( @@ -563,7 +673,7 @@ class PendingScreen extends ConsumerWidget { ), ], ), - child: Center( + child: Center( child: Text( "View Details", style: AppTextStyles.semiBold.copyWith( diff --git a/pubspec.lock b/pubspec.lock index 1bc5064..182912a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -257,6 +257,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.7" + eventify: + dependency: transitive + description: + name: eventify + sha256: b829429f08586cc2001c628e7499e3e3c2493a1d895fd73b00ecb23351aa5a66 + url: "https://pub.dev" + source: hosted + version: "1.0.1" fake_async: dependency: transitive description: @@ -944,6 +952,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 4751d7d..d889b13 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: flutter_local_notifications: ^18.0.1 curved_labeled_navigation_bar: ^2.0.6 http_parser: ^4.1.2 + razorpay_flutter: ^1.4.4