feature/payment #77

Closed
manish.zure wants to merge 25 commits from feature/payment into main
9 changed files with 337 additions and 193 deletions
Showing only changes of commit f70138238b - Show all commits

BIN
lib.zip

Binary file not shown.

View File

@ -4,27 +4,16 @@ import 'package:marco/helpers/services/payment_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
class PaymentController with ChangeNotifier {
final Razorpay _razorpay = Razorpay();
Razorpay? _razorpay;
final PaymentService _paymentService = PaymentService();
bool isProcessing = false;
BuildContext? _context; // For showing dialogs/snackbars
PaymentController() {
// Razorpay event listeners
_razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
_razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
_razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
}
void disposeController() {
_razorpay.clear();
}
BuildContext? _context;
/// ==============================
/// START PAYMENT
/// START PAYMENT (Safe init)
/// ==============================
Future<void> startPayment({
Future<bool> startPayment({
required double amount,
required String description,
required BuildContext context,
@ -35,44 +24,74 @@ class PaymentController with ChangeNotifier {
logSafe("🟢 Starting payment for ₹$amount - $description");
// Call backend to create Razorpay order
final result = await _paymentService.createOrder(amount);
// Clear any old instance (prevents freeze on re-init)
_cleanup();
if (result == null) {
_showError(context, "Failed to connect to server.");
isProcessing = false;
notifyListeners();
return;
// Create order first (no login dependency)
Map<String, dynamic>? result;
try {
result = await _paymentService
.createOrder(amount)
.timeout(const Duration(seconds: 10));
} catch (e) {
logSafe("⏱️ API Timeout or Exception while creating order: $e",
level: LogLevel.error);
}
final orderId = result['orderId'];
final key = result['key'];
if (result == null) {
_showError(context, "Failed to connect to server or timeout.");
isProcessing = false;
notifyListeners();
return false;
}
final orderId = result['data']?['orderId'];
final key = result['data']?['key'];
if (orderId == null || key == null) {
_showError(context, "Invalid response from server.");
isProcessing = false;
notifyListeners();
return;
return false;
}
var options = {
// Safe initialization of Razorpay (deferred)
try {
logSafe("🟡 Initializing Razorpay instance...");
_razorpay = Razorpay();
_razorpay?.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
_razorpay?.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
_razorpay?.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
logSafe("✅ Razorpay instance initialized successfully.");
} catch (e) {
logSafe("❌ Razorpay init failed: $e", level: LogLevel.error);
_showError(context, "Payment system initialization failed.");
isProcessing = false;
notifyListeners();
return false;
}
// Prepare payment options
final options = {
'key': key,
'amount': (amount * 100).toInt(), // Razorpay takes amount in paise
'amount': (amount * 100).toInt(),
'name': 'Your Company Name',
'description': description,
'order_id': orderId,
'theme': {'color': '#0D47A1'},
'timeout': 120, // 2 minutes timeout
'timeout': 120, // seconds
};
try {
logSafe("🟠 Opening Razorpay with options: $options");
_razorpay.open(options);
logSafe("🟠 Opening Razorpay checkout with options: $options");
_razorpay!.open(options);
return true;
} catch (e) {
logSafe("❌ Error opening Razorpay: $e", level: LogLevel.error);
_showError(context, "Error opening payment gateway.");
_cleanup();
isProcessing = false;
notifyListeners();
return false;
}
}
@ -81,15 +100,21 @@ class PaymentController with ChangeNotifier {
/// ==============================
void _handlePaymentSuccess(PaymentSuccessResponse response) async {
logSafe("✅ Payment Success: ${response.paymentId}");
isProcessing = true;
notifyListeners();
final result = await _paymentService.verifyPayment(
paymentId: response.paymentId!,
orderId: response.orderId!,
signature: response.signature!,
);
Map<String, dynamic>? result;
try {
result = await _paymentService
.verifyPayment(
paymentId: response.paymentId!,
orderId: response.orderId!,
signature: response.signature!,
)
.timeout(const Duration(seconds: 10));
} catch (e) {
logSafe("⏱️ Verification timeout/error: $e", level: LogLevel.error);
}
isProcessing = false;
notifyListeners();
@ -107,6 +132,8 @@ class PaymentController with ChangeNotifier {
success: false,
);
}
_cleanup();
}
void _handlePaymentError(PaymentFailureResponse response) {
@ -119,6 +146,8 @@ class PaymentController with ChangeNotifier {
message: "Reason: ${response.message ?? 'Unknown error'}",
success: false,
);
_cleanup();
}
void _handleExternalWallet(ExternalWalletResponse response) {
@ -126,7 +155,25 @@ class PaymentController with ChangeNotifier {
}
/// ==============================
/// HELPER DIALOGS / SNACKBARS
/// CLEANUP / DISPOSE
/// ==============================
void _cleanup() {
try {
_razorpay?.clear();
} catch (_) {}
_razorpay = null;
}
@override
void dispose() {
_cleanup();
super.dispose();
}
void disposeController() => _cleanup();
/// ==============================
/// HELPER UI FUNCTIONS
/// ==============================
void _showDialog({
required String title,
@ -145,9 +192,14 @@ class PaymentController with ChangeNotifier {
child: const Text("OK"),
onPressed: () {
Navigator.of(ctx).pop();
if (success) {
Navigator.of(_context!).pop(true); // Return success
}
ScaffoldMessenger.of(_context!).showSnackBar(
SnackBar(
content: Text(success
? "Payment verified successfully!"
: "Payment failed or could not be verified."),
backgroundColor: success ? Colors.green : Colors.red,
),
);
},
),
],

View File

@ -102,6 +102,6 @@ class ApiEndpoints {
static const String getAssignedServices = "/Project/get/assigned/services";
// Payment Module API Endpoints
static const String createOrder = "/api/payment/create-order";
static const String verifyPayment = "/api/payment/verify-payment";
static const String createOrder = "/payment/create-order";
static const String verifyPayment = "/payment/verify-payment";
}

View File

@ -75,10 +75,17 @@ class ApiService {
return token;
}
static Map<String, String> _headers(String token) => {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
};
static Map<String, String> _headers(String? token) {
final headers = {
'Content-Type': 'application/json',
};
// 👇 Only add Authorization header if token is available
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
return headers;
}
static void _log(String message) {
if (enableLogs) logSafe(message);
@ -123,32 +130,49 @@ class ApiService {
String endpoint, {
Map<String, String>? queryParams,
bool hasRetried = false,
bool requireAuth = true,
}) async {
String? token = await _getToken();
if (token == null) {
logSafe("Token is null. Forcing logout from GET request.",
level: LogLevel.error);
await LocalStorage.logout();
return null;
// Allow public (no-login) API calls for subscription & payment
final isPublicEndpoint =
endpoint.contains("/subscription") || endpoint.contains("/payment");
String? token;
if (requireAuth && !isPublicEndpoint) {
token = await _getToken();
if (token == null) {
logSafe("⛔ Token is null. Forcing logout from GET request.",
level: LogLevel.error);
await LocalStorage.logout();
return null;
}
}
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
.replace(queryParameters: queryParams);
logSafe("Initiating GET request", level: LogLevel.debug);
logSafe("🌐 Initiating GET request", level: LogLevel.debug);
logSafe("URL: $uri", level: LogLevel.debug);
logSafe("Query Parameters: ${queryParams ?? {}}", level: LogLevel.debug);
logSafe("Headers: ${_headers(token)}", level: LogLevel.debug);
try {
final response = await http
.get(uri, headers: _headers(token))
.get(
uri,
headers: (isPublicEndpoint || !requireAuth)
? _headers(null)
: _headers(token),
)
.timeout(extendedTimeout);
logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug);
logSafe("Response Body: ${response.body}", level: LogLevel.debug);
if (response.statusCode == 401 && !hasRetried) {
// 🔒 Retry only for private endpoints
if (response.statusCode == 401 &&
!hasRetried &&
requireAuth &&
!isPublicEndpoint) {
logSafe("Unauthorized (401). Attempting token refresh...",
level: LogLevel.warning);
@ -159,6 +183,7 @@ class ApiService {
endpoint,
queryParams: queryParams,
hasRetried: true,
requireAuth: requireAuth,
);
}
@ -176,33 +201,68 @@ class ApiService {
static Future<http.Response?> _postRequest(
String endpoint,
dynamic body, {
Duration customTimeout = extendedTimeout,
dynamic data, {
// <-- changed from Map<String, dynamic> to dynamic
Map<String, String>? queryParams,
bool hasRetried = false,
bool requireAuth = true,
Duration? customTimeout,
}) async {
String? token = await _getToken();
if (token == null) return null;
// Allow public (no-login) API calls for subscription & payment
final isPublicEndpoint =
endpoint.contains("/subscription") || endpoint.contains("/payment");
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
logSafe(
"POST $uri\nHeaders: ${_headers(token)}\nBody: $body",
);
String? token = await _getToken();
if (token == null && requireAuth && !isPublicEndpoint) {
logSafe("⛔ Token missing for private POST: $endpoint",
level: LogLevel.error);
await LocalStorage.logout();
return null;
}
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
.replace(queryParameters: queryParams);
logSafe("🌐 POST $uri", level: LogLevel.debug);
logSafe("Headers: ${_headers(token)}", level: LogLevel.debug);
logSafe("Body: $data", level: LogLevel.debug);
try {
final response = await http
.post(uri, headers: _headers(token), body: jsonEncode(body))
.timeout(customTimeout);
.post(
uri,
headers: (isPublicEndpoint || !requireAuth)
? _headers(null)
: _headers(token),
body: jsonEncode(data), // handles both Map and List
)
.timeout(customTimeout ?? extendedTimeout);
if (response.statusCode == 401 && !hasRetried) {
logSafe("Unauthorized POST. Attempting token refresh...");
logSafe("Response ${response.statusCode}: ${response.body}",
level: LogLevel.debug);
// Retry token refresh for private routes only
if (response.statusCode == 401 &&
!hasRetried &&
requireAuth &&
!isPublicEndpoint) {
if (await AuthService.refreshToken()) {
return await _postRequest(endpoint, body,
customTimeout: customTimeout, hasRetried: true);
return await _postRequest(
endpoint,
data,
queryParams: queryParams,
hasRetried: true,
requireAuth: requireAuth,
customTimeout: customTimeout,
);
}
await LocalStorage.logout();
}
return response;
} catch (e) {
logSafe("HTTP POST Exception: $e", level: LogLevel.error);
logSafe("HTTP POST Exception: $e", level: LogLevel.error);
return null;
}
}
@ -1963,11 +2023,12 @@ class ApiService {
static Future<Map<String, dynamic>?> getSubscriptionPlans(
String frequency) async {
try {
final endpoint =
"/api/market/list/subscription-plan?frequency=$frequency";
final endpoint = "/market/list/subscription-plan?frequency=$frequency";
logSafe("Fetching subscription plans for frequency: $frequency");
final response = await _getRequest(endpoint);
// 👇 Pass `requireAuth: false` to make this API public
final response = await _getRequest(endpoint, requireAuth: false);
if (response == null) {
logSafe("Subscription plans request failed: null response",
level: LogLevel.error);
@ -2135,11 +2196,18 @@ class ApiService {
static Future<Map<String, dynamic>?> createPaymentOrder(double amount) async {
const endpoint = ApiEndpoints.createOrder; // endpoint for order creation
try {
final response = await _postRequest(endpoint, {'amount': amount});
// Allow this API call without requiring login/token
final response = await _postRequest(
endpoint,
{'amount': amount},
requireAuth: false, // 👈 this is the key change
);
if (response == null) return null;
return _parseResponse(response, label: "Create Payment Order");
} catch (e) {
logSafe("Exception during createPaymentOrder: $e", level: LogLevel.error);
logSafe("❌ Exception during createPaymentOrder: $e",
level: LogLevel.error);
return null;
}
}
@ -2152,11 +2220,15 @@ class ApiService {
}) async {
const endpoint = ApiEndpoints.verifyPayment;
try {
final response = await _postRequest(endpoint, {
'orderId': orderId,
'paymentId': paymentId,
'signature': signature,
});
final response = await _postRequest(
endpoint,
{
'orderId': orderId,
'paymentId': paymentId,
'signature': signature,
},
requireAuth: false,
);
if (response == null) return null;
return _parseResponse(response, label: "Verify Payment");
} catch (e) {

View File

@ -1,4 +1,3 @@
// payment_service.dart
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';

View File

@ -25,112 +25,120 @@ import 'package:marco/view/payment/payment_screen.dart';
import 'package:marco/view/subscriptions/subscriptions_screen.dart';
class AuthMiddleware extends GetMiddleware {
@override
RouteSettings? redirect(String? route) {
if (!AuthService.isLoggedIn) {
if (route != '/auth/login-option') {
return const RouteSettings(name: '/auth/login-option');
}
} else if (!TenantService.isTenantSelected) {
if (route != '/select-tenant') {
return const RouteSettings(name: '/select-tenant');
}
}
return null;
}
@override
RouteSettings? redirect(String? route) {
// Public routes (no auth required)
const publicRoutes = [
'/auth/login-option',
'/auth/login',
'/subscription', // 👈 Allow this route without auth
'/payment',
'/select-tenant',
];
// Skip auth checks for public routes
if (publicRoutes.contains(route)) return null;
if (!AuthService.isLoggedIn) {
return const RouteSettings(name: '/auth/login-option');
}
if (!TenantService.isTenantSelected) {
return const RouteSettings(name: '/select-tenant');
}
return null;
}
}
getPageRoute() {
var routes = [
GetPage(
name: '/',
page: () => DashboardScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard',
page: () => DashboardScreen(), // or your actual home screen
middlewares: [AuthMiddleware()],
),
GetPage(
name: '/select-tenant',
page: () => const TenantSelectionScreen(),
middlewares: [AuthMiddleware()]),
var routes = [
GetPage(
name: '/',
page: () => DashboardScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard',
page: () => DashboardScreen(), // or your actual home screen
middlewares: [AuthMiddleware()],
),
GetPage(
name: '/select-tenant',
page: () => const TenantSelectionScreen(),
middlewares: [AuthMiddleware()]),
// Dashboard
GetPage(
name: '/dashboard/attendance',
page: () => AttendanceScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard',
page: () => DashboardScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/employees',
page: () => EmployeesScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/attendance',
page: () => AttendanceScreen(),
middlewares: [AuthMiddleware()]),
// GetPage(
// name: '/dashboard',
// page: () => DashboardScreen(),
// middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/employees',
page: () => EmployeesScreen(),
middlewares: [AuthMiddleware()]),
// Daily Task Planning
GetPage(
name: '/dashboard/daily-task-Planning',
page: () => DailyTaskPlanningScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/daily-task-progress',
page: () => DailyProgressReportScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/directory-main-page',
page: () => DirectoryMainScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/daily-task-Planning',
page: () => DailyTaskPlanningScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/daily-task-progress',
page: () => DailyProgressReportScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/directory-main-page',
page: () => DirectoryMainScreen(),
middlewares: [AuthMiddleware()]),
// Expense
GetPage(
name: '/dashboard/expense-main-page',
page: () => ExpenseMainScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/expense-main-page',
page: () => ExpenseMainScreen(),
middlewares: [AuthMiddleware()]),
// Documents
GetPage(
name: '/dashboard/document-main-page',
page: () => UserDocumentsPage(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/document-main-page',
page: () => UserDocumentsPage(),
middlewares: [AuthMiddleware()]),
// Payment
GetPage(
name: '/dashboard/payment',
page: () => PaymentScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/subscription',
page: () => SubscriptionScreen(),
middlewares: [AuthMiddleware()]),
GetPage(name: '/payment', page: () => PaymentScreen()),
GetPage(
name: '/subscription',
page: () => SubscriptionScreen(),
),
// Authentication
GetPage(name: '/auth/login', page: () => LoginScreen()),
GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()),
GetPage(name: '/auth/mpin', page: () => MPINScreen()),
GetPage(name: '/auth/mpin-auth', page: () => MPINAuthScreen()),
GetPage(
name: '/auth/register_account',
page: () => const RegisterAccountScreen()),
GetPage(name: '/auth/forgot_password', page: () => ForgotPasswordScreen()),
GetPage(
name: '/auth/reset_password', page: () => const ResetPasswordScreen()),
GetPage(name: '/auth/login', page: () => LoginScreen()),
GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()),
GetPage(name: '/auth/mpin', page: () => MPINScreen()),
GetPage(name: '/auth/mpin-auth', page: () => MPINAuthScreen()),
GetPage(
name: '/auth/register_account',
page: () => const RegisterAccountScreen()),
GetPage(name: '/auth/forgot_password', page: () => ForgotPasswordScreen()),
GetPage(
name: '/auth/reset_password', page: () => const ResetPasswordScreen()),
// Error
GetPage(
name: '/error/coming_soon',
page: () => ComingSoonScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/error/500',
page: () => Error500Screen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/error/404',
page: () => Error404Screen(),
middlewares: [AuthMiddleware()]),
];
return routes
.map((e) => GetPage(
name: e.name,
page: e.page,
middlewares: e.middlewares,
transition: Transition.noTransition))
.toList();
}
GetPage(
name: '/error/coming_soon',
page: () => ComingSoonScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/error/500',
page: () => Error500Screen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/error/404',
page: () => Error404Screen(),
middlewares: [AuthMiddleware()]),
];
return routes
.map((e) => GetPage(
name: e.name,
page: e.page,
middlewares: e.middlewares,
transition: Transition.noTransition))
.toList();
}

View File

@ -7,6 +7,8 @@ import 'package:marco/view/auth/otp_login_form.dart';
import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/view/auth/request_demo_bottom_sheet.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:get/get.dart';
enum LoginOption { email, otp }
@ -133,6 +135,13 @@ class _WelcomeScreenState extends State<WelcomeScreen>
option: LoginOption.otp,
),
const SizedBox(height: 16),
_buildActionButton(
context,
label: "Subscribe",
icon: LucideIcons.bell,
option: null,
),
const SizedBox(height: 16),
_buildActionButton(
context,
label: "Request a Demo",
@ -237,6 +246,10 @@ class _WelcomeScreenState extends State<WelcomeScreen>
shadowColor: Colors.black26,
),
onPressed: () {
if (label == "Subscribe") {
Get.toNamed('/subscription'); // Navigate to Subscription screen
return;
}
if (option == null) {
OrganizationFormBottomSheet.show(context);
} else {

View File

@ -303,14 +303,14 @@ class _UserProfileBarState extends State<UserProfileBar>
onTap: _onProfileTap,
),
SizedBox(height: spacingHeight),
_menuItemRow(
icon: LucideIcons.bell,
label: 'Subscribe',
onTap: _onSubscribeTap,
iconColor: Colors.redAccent,
textColor: Colors.redAccent,
),
SizedBox(height: spacingHeight),
// _menuItemRow(
// icon: LucideIcons.bell,
// label: 'Subscribe',
// onTap: _onSubscribeTap,
// iconColor: Colors.redAccent,
// textColor: Colors.redAccent,
// ),
// SizedBox(height: spacingHeight),
_menuItemRow(
icon: LucideIcons.settings,
label: 'Settings',
@ -380,9 +380,9 @@ class _UserProfileBarState extends State<UserProfileBar>
));
}
void _onSubscribeTap() {
Get.toNamed("/subscription");
}
// void _onSubscribeTap() {
// Get.toNamed("/subscription");
// }
void _onMpinTap() {

View File

@ -10,7 +10,7 @@ class PaymentScreen extends StatefulWidget {
const PaymentScreen({
super.key,
this.amount = 0.0,
this.description = "No description",
this.description = "No description",
});
@override
@ -66,8 +66,8 @@ class _PaymentScreenState extends State<PaymentScreen> {
final controller = _controller!;
return Scaffold(
appBar: AppBar(
title: const Text("Payment"),
backgroundColor: Colors.blueAccent,
title: const Text("Payment", style: TextStyle(color: Colors.black),),
backgroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16.0),