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