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: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 {
static final Dio _dio = Dio(BaseOptions(
@ -10,6 +13,7 @@ class DioClient {
'Accept': 'application/json',
},
))
..interceptors.add(AuthInterceptor())
..interceptors.add(
AwesomeDioInterceptor(
logRequestTimeout: true,
@ -20,3 +24,158 @@ class DioClient {
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 '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/customer_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/file_remote_datasource.dart';
@ -285,6 +286,7 @@ class _MyAppState extends State<MyApp> {
),
],
child: MaterialApp(
navigatorKey: AuthInterceptor.navigatorKey,
debugShowCheckedModeBanner: false,
title: 'POS Resto App',
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/data/models/response/order_response_model.dart';
import 'package:flutter/material.dart';
@ -40,7 +41,10 @@ class ConfirmVoidDialog extends StatelessWidget {
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red[400]!, Colors.red[600]!],
colors: [
const Color.fromARGB(255, 77, 45, 120),
AppColors.primary
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
@ -118,9 +122,11 @@ class ConfirmVoidDialog extends StatelessWidget {
Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.orange[50],
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange[200]!),
border: Border.all(
color: AppColors.primary,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -132,12 +138,12 @@ class ConfirmVoidDialog extends StatelessWidget {
Container(
padding: EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.orange[100],
color: AppColors.primary.withOpacity(0.2),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
Icons.list_alt_rounded,
color: Colors.orange[700],
color: AppColors.primary,
size: 16,
),
),
@ -147,7 +153,7 @@ class ConfirmVoidDialog extends StatelessWidget {
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.orange[800],
color: AppColors.primary,
),
),
],
@ -186,7 +192,7 @@ class ConfirmVoidDialog extends StatelessWidget {
width: 6,
height: 6,
decoration: BoxDecoration(
color: Colors.red[400],
color: AppColors.primary,
shape: BoxShape.circle,
),
),
@ -222,7 +228,8 @@ class ConfirmVoidDialog extends StatelessWidget {
padding: EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.red[100],
color: AppColors.primary
.withOpacity(0.2),
borderRadius:
BorderRadius.circular(4),
),
@ -233,7 +240,7 @@ class ConfirmVoidDialog extends StatelessWidget {
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.red[700],
color: AppColors.primary,
),
),
),
@ -353,7 +360,7 @@ class ConfirmVoidDialog extends StatelessWidget {
),
SizedBox(width: 12),
Expanded(
child: Container(
child: SizedBox(
height: 44,
child: ElevatedButton(
onPressed: () {
@ -361,10 +368,10 @@ class ConfirmVoidDialog extends StatelessWidget {
onTap();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
elevation: 2,
shadowColor: Colors.red.withOpacity(0.3),
shadowColor: AppColors.primary.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),

View File

@ -112,14 +112,6 @@ class _VoidPageState extends State<VoidPage> {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -317,14 +309,6 @@ class _VoidPageState extends State<VoidPage> {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,