feat: handler ui and update void ui

This commit is contained in:
efrilm 2025-08-07 19:57:59 +07:00
parent fbd59964c3
commit b5ad20eb8b
4 changed files with 181 additions and 29 deletions

View File

@ -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,
);
}
}
}

View File

@ -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,

View File

@ -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),
), ),

View File

@ -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,