refactor: implement centralized Odoo session expiration handling with automatic login redirection and unify service calls
This commit is contained in:
parent
b2363b6c6b
commit
82cc2534bc
@ -56,6 +56,8 @@ void main() async {
|
||||
runApp(OdooLoyaltyApp(homeWidget: homeWidget));
|
||||
}
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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<LoginScreen> createState() => _LoginScreenState();
|
||||
@ -27,6 +28,16 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
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<void> _loadAppConfig() async {
|
||||
|
||||
@ -58,10 +58,9 @@ class _MainShellState extends State<MainShell> {
|
||||
|
||||
Future<void> _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},
|
||||
|
||||
@ -26,10 +26,9 @@ class _NotificationsScreenState extends State<NotificationsScreen> {
|
||||
|
||||
Future<void> _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<NotificationsScreen> {
|
||||
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());
|
||||
|
||||
@ -376,7 +376,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
const SizedBox(height: 20),
|
||||
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedGender,
|
||||
initialValue: _selectedGender,
|
||||
items: _genderOptions.map((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
|
||||
@ -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<void> _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<T> _performWithSessionCheck<T>(Future<T> 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<dynamic> callKw(Map<String, dynamic> params) async {
|
||||
if (client == null) throw Exception("Connect to Odoo first");
|
||||
return _performWithSessionCheck(() => client!.callKw(params));
|
||||
}
|
||||
|
||||
Future<dynamic> 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<OdooSession> 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<List<dynamic>> 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<List<dynamic>> 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<Map<String, dynamic>> 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<List<dynamic>> 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<dynamic> 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',
|
||||
{
|
||||
|
||||
@ -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());
|
||||
|
||||
Loading…
Reference in New Issue
Block a user