diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart index 986e5dd..309d3b5 100644 --- a/lib/core/network/dio_client.dart +++ b/lib/core/network/dio_client.dart @@ -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 navigatorKey = + GlobalKey(); + + @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 _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 navigatorKey = + GlobalKey(); + + @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 _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 _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 _handleTokenExpired() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.clear(); + + final context = navigatorKey.currentContext; + if (context != null) { + Navigator.of(context).pushNamedAndRemoveUntil( + '/login', + (route) => false, + ); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 2c0c671..aaafa5e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 { ), ], child: MaterialApp( + navigatorKey: AuthInterceptor.navigatorKey, debugShowCheckedModeBanner: false, title: 'POS Resto App', theme: getApplicationTheme, diff --git a/lib/presentation/void/dialog/confirm_void_dialog.dart b/lib/presentation/void/dialog/confirm_void_dialog.dart index b9e1a0d..2963020 100644 --- a/lib/presentation/void/dialog/confirm_void_dialog.dart +++ b/lib/presentation/void/dialog/confirm_void_dialog.dart @@ -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), ), diff --git a/lib/presentation/void/pages/void_page.dart b/lib/presentation/void/pages/void_page.dart index 5cbbf7c..5de4ad0 100644 --- a/lib/presentation/void/pages/void_page.dart +++ b/lib/presentation/void/pages/void_page.dart @@ -112,14 +112,6 @@ class _VoidPageState extends State { 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 { 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,