diff --git a/lib/main.dart b/lib/main.dart index b9abbd5..d932a45 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -56,6 +56,8 @@ void main() async { runApp(OdooLoyaltyApp(homeWidget: homeWidget)); } +final GlobalKey navigatorKey = GlobalKey(); + class OdooLoyaltyApp extends StatelessWidget { final Widget homeWidget; const OdooLoyaltyApp({super.key, required this.homeWidget}); @@ -68,6 +70,7 @@ class OdooLoyaltyApp extends StatelessWidget { return MaterialApp( title: 'Mie Mapan Loyalty App', debugShowCheckedModeBanner: false, + navigatorKey: navigatorKey, theme: ThemeManager.instance.themeData, home: homeWidget, ); diff --git a/lib/screens/carousel_detail_screen.dart b/lib/screens/carousel_detail_screen.dart index 41d7663..5d86fa2 100644 --- a/lib/screens/carousel_detail_screen.dart +++ b/lib/screens/carousel_detail_screen.dart @@ -96,7 +96,7 @@ class CarouselDetailScreen extends StatelessWidget { width: double.infinity, height: 220, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _placeholder(), + errorBuilder: (_, _, _) => _placeholder(), loadingBuilder: (ctx, child, progress) { if (progress == null) return child; return Container( diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 6057b8d..4c6a1d1 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -12,7 +12,8 @@ import 'forgot_password_screen.dart'; import '../theme/app_theme.dart'; class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); + final bool sessionExpired; + const LoginScreen({super.key, this.sessionExpired = false}); @override State createState() => _LoginScreenState(); @@ -27,6 +28,16 @@ class _LoginScreenState extends State { void initState() { super.initState(); _loadAppConfig(); + if (widget.sessionExpired) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Your session has expired. Please sign in again.'), + backgroundColor: AppTheme.primary, + ), + ); + }); + } } Future _loadAppConfig() async { diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 7561e2b..265ffd9 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -58,10 +58,9 @@ class _MainShellState extends State { Future _fetchNotificationCount() async { try { - final client = OdooService().client; - if (client == null) return; + if (OdooService().client == null) return; - final response = await client.callRPC( + final response = await OdooService().callRPC( '/api/loyalty/fetch_notifications', 'call', {'last_id': 0}, diff --git a/lib/screens/notifications_screen.dart b/lib/screens/notifications_screen.dart index a51f044..3ce64ca 100644 --- a/lib/screens/notifications_screen.dart +++ b/lib/screens/notifications_screen.dart @@ -26,10 +26,9 @@ class _NotificationsScreenState extends State { Future _fetchNotifications() async { try { - final client = OdooService().client; - if (client == null) throw Exception('Not connected'); + if (OdooService().client == null) throw Exception('Not connected'); - final response = await client.callRPC( + final response = await OdooService().callRPC( '/api/loyalty/fetch_notifications', 'call', {'last_id': 0}, @@ -92,7 +91,7 @@ class _NotificationsScreenState extends State { padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 20), itemCount: _notifications.length, - separatorBuilder: (_, __) => const SizedBox(height: 8), + separatorBuilder: (_, _) => const SizedBox(height: 8), itemBuilder: (context, index) { final notif = _notifications[index]; final isUnread = !_readIds.contains((notif['id'] as int? ?? 0).toString()); diff --git a/lib/screens/signup_screen.dart b/lib/screens/signup_screen.dart index bfffa21..49b346f 100644 --- a/lib/screens/signup_screen.dart +++ b/lib/screens/signup_screen.dart @@ -376,7 +376,7 @@ class _SignupScreenState extends State { const SizedBox(height: 20), DropdownButtonFormField( - value: _selectedGender, + initialValue: _selectedGender, items: _genderOptions.map((String value) { return DropdownMenuItem( value: value, diff --git a/lib/services/odoo_service.dart b/lib/services/odoo_service.dart index 1c1c269..8bf35d7 100644 --- a/lib/services/odoo_service.dart +++ b/lib/services/odoo_service.dart @@ -1,4 +1,8 @@ +import 'package:flutter/material.dart'; import 'package:odoo_rpc/odoo_rpc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../main.dart'; +import '../screens/login_screen.dart'; import 'config.dart'; import 'theme_manager.dart'; @@ -31,17 +35,54 @@ class OdooService { String promoImageUrl(int promoId) => '${AppConfig.odooUrl}/web/image/mapan.app.promo/$promoId/image'; + Future _handleSessionExpired() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('odoo_session'); + final state = navigatorKey.currentState; + if (state != null) { + state.pushAndRemoveUntil( + MaterialPageRoute( + builder: (_) => const LoginScreen(sessionExpired: true), + ), + (route) => false, + ); + } + } + + Future _performWithSessionCheck(Future Function() operation) async { + try { + return await operation(); + } on OdooSessionExpiredException { + await _handleSessionExpired(); + rethrow; + } catch (e) { + if (e.toString().toLowerCase().contains('session expired') || + e.toString().toLowerCase().contains('session_expired')) { + await _handleSessionExpired(); + } + rethrow; + } + } + + Future callKw(Map params) async { + if (client == null) throw Exception("Connect to Odoo first"); + return _performWithSessionCheck(() => client!.callKw(params)); + } + + Future callRPC(String path, String method, dynamic params) async { + if (client == null) throw Exception("Connect to Odoo first"); + return _performWithSessionCheck(() => client!.callRPC(path, method, params)); + } + Future login(String db, String username, String password) async { if (client == null) throw Exception("Connect to Odoo first"); return await client!.authenticate(db, username, password); } Future> getLoyaltyCards(int partnerId) async { - if (client == null) throw Exception("Connect to Odoo first"); - // Only fetch cards from 'loyalty' type programs (multi-tier: Silver/Gold/Platinum). // Excludes subscriptions, coupons, gift cards, promotions, eWallets, etc. - return await client!.callKw({ + return await callKw({ 'model': 'loyalty.card', 'method': 'search_read', 'args': [ @@ -65,9 +106,7 @@ class OdooService { /// Fetch subscription cards only — displayed as a "My Subscriptions" list. Future> getSubscriptionCards(int partnerId) async { - if (client == null) throw Exception("Connect to Odoo first"); - - return await client!.callKw({ + return await callKw({ 'model': 'loyalty.card', 'method': 'search_read', 'args': [ @@ -91,8 +130,7 @@ class OdooService { /// Fetch carousel slides and promo highlights for the home screen. Future> getCmsContent() async { - if (client == null) throw Exception("Connect to Odoo first"); - final response = await client!.callRPC( + final response = await callRPC( '/api/loyalty/cms_content', 'call', {}, @@ -141,8 +179,7 @@ class OdooService { /// Fetch loyalty point history for the current user. Future> getOrderHistory() async { - if (client == null) throw Exception("Connect to Odoo first"); - final response = await client!.callRPC( + final response = await callRPC( '/api/loyalty/order_history', 'call', {}, @@ -159,14 +196,13 @@ class OdooService { String? phoneOrEmail, required String type, }) async { - if (client == null) throw Exception("Connect to Odoo first"); - return await client!.callRPC( + return await callRPC( '/api/loyalty/send_otp', 'call', { - if (email != null) 'email': email, - if (phone != null) 'phone': phone, - if (phoneOrEmail != null) 'phone_or_email': phoneOrEmail, + 'email': ?email, + 'phone': ?phone, + 'phone_or_email': ?phoneOrEmail, 'type': type, }, ); @@ -181,8 +217,7 @@ class OdooService { required String password, required String otp, }) async { - if (client == null) throw Exception("Connect to Odoo first"); - return await client!.callRPC( + return await callRPC( '/api/loyalty/signup_member', 'call', { @@ -204,8 +239,7 @@ class OdooService { required String password, required String otp, }) async { - if (client == null) throw Exception("Connect to Odoo first"); - return await client!.callRPC( + return await callRPC( '/api/loyalty/activate_account', 'call', { @@ -223,8 +257,7 @@ class OdooService { required String otp, required String password, }) async { - if (client == null) throw Exception("Connect to Odoo first"); - return await client!.callRPC( + return await callRPC( '/api/loyalty/reset_password', 'call', { @@ -236,8 +269,7 @@ class OdooService { } Future deleteAccount(String password) async { - if (client == null) throw Exception("Connect to Odoo first"); - return await client!.callRPC( + return await callRPC( '/api/loyalty/delete_account', 'call', { diff --git a/lib/widgets/carousel_widget.dart b/lib/widgets/carousel_widget.dart index 4be6729..8fe168e 100644 --- a/lib/widgets/carousel_widget.dart +++ b/lib/widgets/carousel_widget.dart @@ -121,7 +121,7 @@ class _SlideImage extends StatelessWidget { externalUrl, fit: BoxFit.cover, width: double.infinity, - errorBuilder: (_, __, ___) => _placeholder(colorScheme), + errorBuilder: (_, _, _) => _placeholder(colorScheme), loadingBuilder: (ctx, child, progress) { if (progress == null) return child; return const Center(child: CircularProgressIndicator());