Commit e9cf8244 authored by DatHV's avatar DatHV
Browse files

update fix bug, handle error.

parent a0bcdab2
...@@ -10,13 +10,19 @@ import '../../base/base_screen.dart'; ...@@ -10,13 +10,19 @@ import '../../base/base_screen.dart';
import '../../base/basic_state.dart'; import '../../base/basic_state.dart';
import '../../directional/directional_screen.dart'; import '../../directional/directional_screen.dart';
import '../../widgets/custom_navigation_bar.dart'; import '../../widgets/custom_navigation_bar.dart';
import '../../preference/data_preference.dart';
import '../../preference/package_info.dart';
class BaseWebViewInput { class BaseWebViewInput {
final String? title; final String? title;
final String url; final String url;
final bool isFullScreen; final bool isFullScreen;
const BaseWebViewInput({this.title, required this.url, this.isFullScreen = false}); const BaseWebViewInput({
this.title,
required this.url,
this.isFullScreen = false,
});
} }
class BaseWebViewScreen extends BaseScreen { class BaseWebViewScreen extends BaseScreen {
...@@ -26,10 +32,13 @@ class BaseWebViewScreen extends BaseScreen { ...@@ -26,10 +32,13 @@ class BaseWebViewScreen extends BaseScreen {
State<BaseWebViewScreen> createState() => _BaseWebViewScreenState(); State<BaseWebViewScreen> createState() => _BaseWebViewScreenState();
} }
class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicState { class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen>
with BasicState {
late final BaseWebViewInput input; late final BaseWebViewInput input;
WebViewController? _controller; // Nullable cho web platform WebViewController? _controller; // Nullable cho web platform
String? _dynamicTitle; String? _dynamicTitle;
Map<String, String>? _authHeaders;
bool _isReissuingNavigation = false;
@override @override
void initState() { void initState() {
...@@ -43,7 +52,6 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta ...@@ -43,7 +52,6 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
}); });
return; return;
} }
// Web platform: mở URL trong tab mới và đóng màn hình ngay // Web platform: mở URL trong tab mới và đóng màn hình ngay
if (kIsWeb) { if (kIsWeb) {
AppLoading().hide(); AppLoading().hide();
...@@ -57,41 +65,40 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta ...@@ -57,41 +65,40 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
}); });
return; return;
} }
// Mobile platform: khởi tạo WebView // Mobile platform: khởi tạo WebView
AppLoading().show(); AppLoading().show();
_controller = WebViewController() _controller =
..setJavaScriptMode(JavaScriptMode.unrestricted) WebViewController()
..setBackgroundColor(Colors.transparent) ..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate( ..setBackgroundColor(Colors.transparent)
NavigationDelegate( ..setNavigationDelegate(
onPageFinished: (_) async { NavigationDelegate(
AppLoading().hide(); onPageFinished: (_) async {
final title = await _controller!.getTitle(); AppLoading().hide();
setState(() { final title = await _controller!.getTitle();
_dynamicTitle = title; setState(() {
}); _dynamicTitle = title;
}, });
onWebResourceError: (error) { },
AppLoading().hide(); onWebResourceError: (error) {
if (error.description != 'about:blank') { AppLoading().hide();
if (kDebugMode) { if (error.description != 'about:blank') {
print('WebView error: ${error.description}'); if (kDebugMode) {
} print('WebView error: ${error.description}');
// Có thể hiển thị lỗi nếu cần }
// showAlertError(content: error.description); // Có thể hiển thị lỗi nếu cần
} showAlertError(content: error.description);
}, }
onNavigationRequest: _handleNavigation, },
), onNavigationRequest: _handleNavigation,
) ),
..loadRequest(Uri.parse(formatUrl(input.url))); );
_clearCookies(); _prepareInitialLoad();
} }
String formatUrl(String inputUrl) { String formatUrl(String inputUrl) {
if (inputUrl.isEmpty) return ''; if (inputUrl.isEmpty) return '';
try { try {
if (!inputUrl.startsWith('http://') && !inputUrl.startsWith('https://')) { if (!inputUrl.startsWith('http://') && !inputUrl.startsWith('https://')) {
return 'https://$inputUrl'; return 'https://$inputUrl';
...@@ -111,29 +118,34 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta ...@@ -111,29 +118,34 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
if (kIsWeb) { if (kIsWeb) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
// Mobile platform: sử dụng WebView // Mobile platform: sử dụng WebView
return Scaffold( return Scaffold(
appBar: input.isFullScreen appBar:
? null input.isFullScreen
: CustomNavigationBar( ? null
title: input.title ?? _dynamicTitle ?? Uri.parse(input.url).host, : CustomNavigationBar(
leftButtons: [ title:
CustomBackButton(onPressed: _handleBack), input.title ?? _dynamicTitle ?? Uri.parse(input.url).host,
], leftButtons: [CustomBackButton(onPressed: _handleBack)],
), ),
body: Stack( body: Stack(
children: [ children: [
SafeArea( SafeArea(
child: WebViewWidget( child: WebViewWidget(
controller: _controller!, controller: _controller!,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{ gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{
Factory<VerticalDragGestureRecognizer>(VerticalDragGestureRecognizer.new), Factory<VerticalDragGestureRecognizer>(
VerticalDragGestureRecognizer.new,
),
}, },
), ),
), ),
if (input.isFullScreen) if (input.isFullScreen)
Positioned(top: MediaQuery.of(context).padding.top + 8, left: 8, child: CustomBackButton()), Positioned(
top: MediaQuery.of(context).padding.top + 8,
left: 8,
child: CustomBackButton(),
),
], ],
), ),
); );
...@@ -151,7 +163,7 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta ...@@ -151,7 +163,7 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
if (context.mounted) Navigator.of(context).pop(); if (context.mounted) Navigator.of(context).pop();
return; return;
} }
// Mobile: kiểm tra WebView có thể go back không // Mobile: kiểm tra WebView có thể go back không
if (_controller != null && await _controller!.canGoBack()) { if (_controller != null && await _controller!.canGoBack()) {
_controller!.goBack(); _controller!.goBack();
...@@ -192,89 +204,106 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta ...@@ -192,89 +204,106 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
launchUrl(uri); launchUrl(uri);
return NavigationDecision.prevent; return NavigationDecision.prevent;
} }
if (_isReissuingNavigation) {
_isReissuingNavigation = false;
return NavigationDecision.navigate;
}
if (_shouldAttachHeaders(url)) {
try {
final target = Uri.parse(url);
_loadWithHeaders(target);
return NavigationDecision.prevent;
} catch (e) {
if (kDebugMode) {
print('Failed to reissue navigation with headers: $e');
}
}
}
return NavigationDecision.navigate; return NavigationDecision.navigate;
} }
// Widget _buildWebViewForWeb() { Future<void> _prepareInitialLoad() async {
// if (!kIsWeb) return const SizedBox.shrink(); await _clearCookies();
// final formattedUrl = formatUrl(input.url);
// return Container( if (formattedUrl.isEmpty) {
// width: double.infinity, AppLoading().hide();
// height: double.infinity, return;
// child: Column( }
// children: [ var uri = Uri.parse(formattedUrl);
// // Header với URL final token = DataPreference.instance.token?.trim();
// Container( if (token != null &&
// padding: const EdgeInsets.all(16), token.isNotEmpty &&
// color: Colors.grey[100], defaultTargetPlatform == TargetPlatform.iOS) {
// child: Row( final query = Map<String, String>.from(uri.queryParameters);
// children: [ query['access_token'] = token;
// Icon(Icons.language, color: Colors.blue), uri = uri.replace(queryParameters: query);
// const SizedBox(width: 8), }
// Expanded( await _syncAuthCookie(uri);
// child: Text( final headers = await _buildInitialHeaders();
// input.url, _authHeaders = headers.isEmpty ? null : headers;
// style: const TextStyle(fontSize: 14), try {
// overflow: TextOverflow.ellipsis, await _loadWithHeaders(uri);
// ), } catch (e) {
// ), if (kDebugMode) {
// const SizedBox(width: 8), print(
// ElevatedButton.icon( 'WebView load with headers failed: $e. Retrying without headers.',
// onPressed: () => _openUrlInBrowser(), );
// icon: const Icon(Icons.open_in_new, size: 16), }
// label: const Text('Mở tab mới'), await _controller?.loadRequest(uri);
// style: ElevatedButton.styleFrom( }
// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), }
// ),
// ), Future<void> _syncAuthCookie(Uri uri) async {
// ], if (!DataPreference.instance.logged) return;
// ), final token = DataPreference.instance.token?.trim();
// ), if (token == null || token.isEmpty) return;
// // iframe area final cookieManager = WebViewCookieManager();
// Expanded( await cookieManager.setCookie(
// child: Container( WebViewCookie(
// width: double.infinity, name: 'access_token',
// height: double.infinity, value: token,
// decoration: BoxDecoration( domain: uri.host,
// border: Border.all(color: Colors.grey[300]!), path: '/',
// ), ),
// child: Center( );
// child: Column( }
// mainAxisAlignment: MainAxisAlignment.center,
// children: [ Future<Map<String, String>> _buildInitialHeaders() async {
// Icon(Icons.web, size: 64, color: Colors.grey[400]), if (!DataPreference.instance.logged) return {};
// const SizedBox(height: 16), final token = DataPreference.instance.token?.trim();
// Text( if (token == null || token.isEmpty) return {};
// 'WebView không hỗ trợ trực tiếp trên web', try {
// style: Theme.of(context).textTheme.headlineSmall?.copyWith( final version = await AppInfoHelper.version;
// color: Colors.grey[600], return {
// ), 'access_token': token,
// textAlign: TextAlign.center, 'version': version,
// ), 'Cookie': 'access_token=$token'
// const SizedBox(height: 8), };
// Text( } catch (_) {
// 'Vui lòng click "Mở tab mới" để xem nội dung', return {
// style: Theme.of(context).textTheme.bodyMedium?.copyWith( 'access_token': token,
// color: Colors.grey[500], 'Cookie': 'access_token=$token'
// ), };
// textAlign: TextAlign.center, }
// ), }
// const SizedBox(height: 24),
// ElevatedButton.icon( Future<void> _loadWithHeaders(Uri uri) async {
// onPressed: () => _openUrlInBrowser(), if (_authHeaders == null || _authHeaders!.isEmpty) {
// icon: const Icon(Icons.open_in_new), await _controller?.loadRequest(uri);
// label: const Text('Mở trong tab mới'), return;
// style: ElevatedButton.styleFrom( }
// padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), _isReissuingNavigation = true;
// ), print('➡️ Loading with headers: ${uri.toString()}, headers: $_authHeaders');
// ), await _controller?.loadRequest(uri, headers: _authHeaders!);
// ], }
// ),
// ), bool _shouldAttachHeaders(String url) {
// ), if (_authHeaders == null || _authHeaders!.isEmpty) return false;
// ), if (url.isEmpty || url == 'about:blank') return false;
// ], final lower = url.toLowerCase();
// ), return lower.startsWith('http://') || lower.startsWith('https://');
// ); }
// }
} }
...@@ -3,11 +3,13 @@ import 'package:flutter/foundation.dart'; ...@@ -3,11 +3,13 @@ import 'package:flutter/foundation.dart';
import '../networking/dio_http_service.dart'; import '../networking/dio_http_service.dart';
import '../networking/restful_api_client.dart'; import '../networking/restful_api_client.dart';
import '../networking/restful_api_client_all_request.dart'; import '../networking/restful_api_client_all_request.dart';
import '../preference/data_preference.dart';
class LogoutService { class LogoutService {
LogoutService._(); LogoutService._();
static final RestfulAPIClient _client = RestfulAPIClient(DioHttpService().dio); static final RestfulAPIClient _client = RestfulAPIClient(DioHttpService().dio);
static Future<void> logout() async { static Future<void> logout() async {
if (!DataPreference.instance.logged) return;
try { try {
await _client.logout(); await _client.logout();
} catch (e) { } catch (e) {
......
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart'; import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../configs/constants.dart';
import '../model/auth/login_token_response_model.dart'; import '../model/auth/login_token_response_model.dart';
import '../networking/restful_api_viewmodel.dart'; import '../networking/restful_api_viewmodel.dart';
import '../preference/data_preference.dart'; import '../preference/data_preference.dart';
...@@ -65,15 +66,16 @@ class TokenRefreshService extends RestfulApiViewModel { ...@@ -65,15 +66,16 @@ class TokenRefreshService extends RestfulApiViewModel {
/// Xử lý refresh thất bại - redirect về login /// Xử lý refresh thất bại - redirect về login
Future<void> _handleRefreshFailure() async { Future<void> _handleRefreshFailure() async {
final logged = DataPreference.instance.logged;
await DataPreference.instance.clearData(); await DataPreference.instance.clearData();
for (final callback in _callbacks) { for (final callback in _callbacks) {
callback(false); callback(false);
} }
_callbacks.clear(); _callbacks.clear();
// Redirect về login screen (ensure on UI thread) // Redirect về login screen (ensure on UI thread)
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
AppNavigator.showAuthAlertAndGoLogin('Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.'); if (!logged) return;
AppNavigator.showAuthAlertAndGoLogin(ErrorCodes.tokenInvalidMessage);
}); });
} }
......
...@@ -36,18 +36,16 @@ dependencies: ...@@ -36,18 +36,16 @@ dependencies:
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
shared_preferences: ^2.5.2 shared_preferences: ^2.5.2
json_annotation: ^4.9.0 json_annotation: ^4.9.0
retrofit:
get: ^4.7.2 get: ^4.7.2
fluttertoast: ^8.2.13 fluttertoast: ^8.2.13
logger: logger: ^2.4.0
url_launcher: ^6.3.1 url_launcher: ^6.3.1
flutter_widget_from_html: ^0.17.1 flutter_widget_from_html: ^0.17.1
device_info_plus: ^12.1.0 device_info_plus: ^12.1.0
uuid: ^4.3.3 uuid: ^4.3.3
universal_html: ^2.2.4 universal_html: ^2.2.4
flutter_svg: local_auth: ^2.1.7
local_auth: pin_code_fields: ^8.0.1
pin_code_fields:
intl: ^0.20.2 intl: ^0.20.2
webview_flutter: ^4.2.2 webview_flutter: ^4.2.2
webview_flutter_wkwebview: ^3.9.4 webview_flutter_wkwebview: ^3.9.4
...@@ -65,7 +63,6 @@ dependencies: ...@@ -65,7 +63,6 @@ dependencies:
fl_chart: ^1.1.0 fl_chart: ^1.1.0
mobile_scanner: ^7.0.1 mobile_scanner: ^7.0.1
encrypt: ^5.0.1 encrypt: ^5.0.1
connectivity_plus:
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4
firebase_core: ^4.1.0 firebase_core: ^4.1.0
firebase_messaging: ^16.0.1 firebase_messaging: ^16.0.1
...@@ -78,7 +75,6 @@ dev_dependencies: ...@@ -78,7 +75,6 @@ dev_dependencies:
sdk: flutter sdk: flutter
build_runner: ^2.4.15 build_runner: ^2.4.15
json_serializable: ^6.9.4 json_serializable: ^6.9.4
retrofit_generator:
equatable: ^2.0.7 equatable: ^2.0.7
# The "flutter_lints" package below contains a set of recommended lints to # The "flutter_lints" package below contains a set of recommended lints to
...@@ -158,4 +154,4 @@ flutter_launcher_icons: ...@@ -158,4 +154,4 @@ flutter_launcher_icons:
image_path: assets/icons/pro/app_icon.png image_path: assets/icons/pro/app_icon.png
adaptive_icon_foreground: assets/icons/pro/ic_foreground.png adaptive_icon_foreground: assets/icons/pro/ic_foreground.png
adaptive_icon_background: "#FFFFFF" adaptive_icon_background: "#FFFFFF"
adaptive_icon_monochrome: assets/icons/pro/ic_monochrome.png adaptive_icon_monochrome: assets/icons/pro/ic_monochrome.png
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment