feat: handler ui and update void ui
This commit is contained in:
parent
fbd59964c3
commit
b5ad20eb8b
@ -1,6 +1,9 @@
|
|||||||
// lib/core/network/dio_client.dart
|
|
||||||
import 'package:awesome_dio_interceptor/awesome_dio_interceptor.dart';
|
import 'package:awesome_dio_interceptor/awesome_dio_interceptor.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
|
||||||
|
import 'package:enaklo_pos/presentation/auth/login_page.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class DioClient {
|
class DioClient {
|
||||||
static final Dio _dio = Dio(BaseOptions(
|
static final Dio _dio = Dio(BaseOptions(
|
||||||
@ -10,6 +13,7 @@ class DioClient {
|
|||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
..interceptors.add(AuthInterceptor())
|
||||||
..interceptors.add(
|
..interceptors.add(
|
||||||
AwesomeDioInterceptor(
|
AwesomeDioInterceptor(
|
||||||
logRequestTimeout: true,
|
logRequestTimeout: true,
|
||||||
@ -20,3 +24,158 @@ class DioClient {
|
|||||||
|
|
||||||
static Dio get instance => _dio;
|
static Dio get instance => _dio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AuthInterceptor extends Interceptor {
|
||||||
|
static final GlobalKey<NavigatorState> navigatorKey =
|
||||||
|
GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRequest(
|
||||||
|
RequestOptions options, RequestInterceptorHandler handler) async {
|
||||||
|
// Add token to request headers
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final token = prefs.getString('auth_token');
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.next(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||||
|
handler.next(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||||
|
// Check if error is 401 (Unauthorized) - token expired
|
||||||
|
if (err.response?.statusCode == 401) {
|
||||||
|
await _handleTokenExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleTokenExpired() async {
|
||||||
|
// Clear stored token
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove('auth_token');
|
||||||
|
await prefs.remove('refresh_token');
|
||||||
|
await prefs.clear(); // Optional: clear all user data
|
||||||
|
|
||||||
|
// Navigate to login page
|
||||||
|
final context = navigatorKey.currentContext;
|
||||||
|
if (context != null) {
|
||||||
|
// Option 1: Navigate and remove all previous routes
|
||||||
|
context.pushReplacement(LoginPage());
|
||||||
|
|
||||||
|
// Option 2: If using GoRouter, uncomment below:
|
||||||
|
// GoRouter.of(context).go('/login');
|
||||||
|
|
||||||
|
// Option 3: If using custom routing, uncomment below:
|
||||||
|
// Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
// MaterialPageRoute(builder: (context) => LoginPage()),
|
||||||
|
// (route) => false,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternative: Dengan Refresh Token Logic
|
||||||
|
class AuthInterceptorWithRefresh extends Interceptor {
|
||||||
|
static final GlobalKey<NavigatorState> navigatorKey =
|
||||||
|
GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRequest(
|
||||||
|
RequestOptions options, RequestInterceptorHandler handler) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final token = prefs.getString('auth_token');
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.next(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||||
|
if (err.response?.statusCode == 401) {
|
||||||
|
// Try refresh token first
|
||||||
|
final success = await _tryRefreshToken();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Retry the original request
|
||||||
|
final response = await _retryRequest(err.requestOptions);
|
||||||
|
handler.resolve(response);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Refresh failed, redirect to login
|
||||||
|
await _handleTokenExpired();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _tryRefreshToken() async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final refreshToken = prefs.getString('refresh_token');
|
||||||
|
|
||||||
|
if (refreshToken == null) return false;
|
||||||
|
|
||||||
|
final response = await Dio().post(
|
||||||
|
'YOUR_REFRESH_TOKEN_ENDPOINT',
|
||||||
|
data: {'refresh_token': refreshToken},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final newToken = response.data['access_token'];
|
||||||
|
final newRefreshToken = response.data['refresh_token'];
|
||||||
|
|
||||||
|
await prefs.setString('auth_token', newToken);
|
||||||
|
await prefs.setString('refresh_token', newRefreshToken);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Refresh token failed: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> _retryRequest(RequestOptions options) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final token = prefs.getString('auth_token');
|
||||||
|
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
|
||||||
|
return await DioClient.instance.request(
|
||||||
|
options.path,
|
||||||
|
options: Options(
|
||||||
|
method: options.method,
|
||||||
|
headers: options.headers,
|
||||||
|
),
|
||||||
|
data: options.data,
|
||||||
|
queryParameters: options.queryParameters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleTokenExpired() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.clear();
|
||||||
|
|
||||||
|
final context = navigatorKey.currentContext;
|
||||||
|
if (context != null) {
|
||||||
|
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||||
|
'/login',
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'package:enaklo_pos/core/constants/theme.dart';
|
import 'package:enaklo_pos/core/constants/theme.dart';
|
||||||
|
import 'package:enaklo_pos/core/network/dio_client.dart';
|
||||||
import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart';
|
import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart';
|
||||||
import 'package:enaklo_pos/data/datasources/customer_remote_datasource.dart';
|
import 'package:enaklo_pos/data/datasources/customer_remote_datasource.dart';
|
||||||
import 'package:enaklo_pos/data/datasources/file_remote_datasource.dart';
|
import 'package:enaklo_pos/data/datasources/file_remote_datasource.dart';
|
||||||
@ -285,6 +286,7 @@ class _MyAppState extends State<MyApp> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
|
navigatorKey: AuthInterceptor.navigatorKey,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: 'POS Resto App',
|
title: 'POS Resto App',
|
||||||
theme: getApplicationTheme,
|
theme: getApplicationTheme,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:enaklo_pos/core/constants/colors.dart';
|
||||||
import 'package:enaklo_pos/core/extensions/int_ext.dart';
|
import 'package:enaklo_pos/core/extensions/int_ext.dart';
|
||||||
import 'package:enaklo_pos/data/models/response/order_response_model.dart';
|
import 'package:enaklo_pos/data/models/response/order_response_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -40,7 +41,10 @@ class ConfirmVoidDialog extends StatelessWidget {
|
|||||||
padding: EdgeInsets.all(20),
|
padding: EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [Colors.red[400]!, Colors.red[600]!],
|
colors: [
|
||||||
|
const Color.fromARGB(255, 77, 45, 120),
|
||||||
|
AppColors.primary
|
||||||
|
],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
@ -118,9 +122,11 @@ class ConfirmVoidDialog extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.orange[50],
|
color: AppColors.primary.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: Colors.orange[200]!),
|
border: Border.all(
|
||||||
|
color: AppColors.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -132,12 +138,12 @@ class ConfirmVoidDialog extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.all(6),
|
padding: EdgeInsets.all(6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.orange[100],
|
color: AppColors.primary.withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.list_alt_rounded,
|
Icons.list_alt_rounded,
|
||||||
color: Colors.orange[700],
|
color: AppColors.primary,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -147,7 +153,7 @@ class ConfirmVoidDialog extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.orange[800],
|
color: AppColors.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -186,7 +192,7 @@ class ConfirmVoidDialog extends StatelessWidget {
|
|||||||
width: 6,
|
width: 6,
|
||||||
height: 6,
|
height: 6,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.red[400],
|
color: AppColors.primary,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -222,7 +228,8 @@ class ConfirmVoidDialog extends StatelessWidget {
|
|||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: 6, vertical: 2),
|
horizontal: 6, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.red[100],
|
color: AppColors.primary
|
||||||
|
.withOpacity(0.2),
|
||||||
borderRadius:
|
borderRadius:
|
||||||
BorderRadius.circular(4),
|
BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
@ -233,7 +240,7 @@ class ConfirmVoidDialog extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.red[700],
|
color: AppColors.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -353,7 +360,7 @@ class ConfirmVoidDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: 44,
|
height: 44,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -361,10 +368,10 @@ class ConfirmVoidDialog extends StatelessWidget {
|
|||||||
onTap();
|
onTap();
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red[600],
|
backgroundColor: AppColors.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shadowColor: Colors.red.withOpacity(0.3),
|
shadowColor: AppColors.primary.withOpacity(0.3),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -112,14 +112,6 @@ class _VoidPageState extends State<VoidPage> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
spreadRadius: 1,
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -317,14 +309,6 @@ class _VoidPageState extends State<VoidPage> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
spreadRadius: 1,
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user