From ae3550e6693aa23662389af05750260665642325 Mon Sep 17 00:00:00 2001 From: Developer Date: Sun, 7 Jun 2026 19:28:53 +0700 Subject: [PATCH] Persist H5 WebView login state --- lib/main.dart | 269 ++++++++------------------------------------------ 1 file changed, 39 insertions(+), 230 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 905eae2..0e748b2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,28 +17,16 @@ const _shellAccent = Color(0xFF168CFF); const _shellAccentDeep = Color(0xFF0066D9); const _shellInk = Color(0xFF17233D); const _shellSubText = Color(0xFF7C8AA3); -const _shellBrandingChannel = - MethodChannel('io.openim.flutter.im_webview_app/shell_branding'); -const _androidFilePickerChannel = - MethodChannel('io.openim.flutter.openim/file_picker'); -const _h5CacheChannel = - MethodChannel('io.openim.flutter.im_webview_app/h5_cache'); +const _shellBrandingChannel = MethodChannel( + 'io.openim.flutter.im_webview_app/shell_branding', +); +const _androidFilePickerChannel = MethodChannel( + 'io.openim.flutter.openim/file_picker', +); const _keyboardAnimationDuration = Duration(milliseconds: 250); -const _initialH5CacheClearedKey = 'initial_h5_cache_cleared_v1'; -const _h5DomainCacheDateKey = 'h5_random_domain_date_v1'; -const _h5DomainCacheSessionKey = 'h5_random_domain_session_v1'; const _h5DomainCacheUrlKey = 'h5_random_domain_url_v1'; const _lineProbeTimeout = Duration(seconds: 5); const _maxGeneratedLineAttempts = 6; -final String _h5DomainProcessSession = - 'session-${DateTime.now().microsecondsSinceEpoch}'; - -String _domainDateKey(DateTime value) { - final year = value.year.toString().padLeft(4, '0'); - final month = value.month.toString().padLeft(2, '0'); - final day = value.day.toString().padLeft(2, '0'); - return '$year-$month-$day'; -} Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -54,10 +42,7 @@ Future main() async { } class ShellBranding { - const ShellBranding({ - required this.appName, - required this.appLogo, - }); + const ShellBranding({required this.appName, required this.appLogo}); static const fallback = ShellBranding( appName: AppConfig.appName, @@ -69,8 +54,9 @@ class ShellBranding { static Future load() async { try { - final data = await _shellBrandingChannel - .invokeMapMethod('getShellBranding'); + final data = await _shellBrandingChannel.invokeMapMethod( + 'getShellBranding', + ); final appName = _trim(data?['appName']); final appLogo = _trim(data?['appLogo']); @@ -86,17 +72,10 @@ class ShellBranding { static String _trim(String? value) => value?.trim() ?? ''; } -enum _LineAvailability { - checking, - available, - unavailable, -} +enum _LineAvailability { checking, available, unavailable } class ImWebViewApp extends StatelessWidget { - const ImWebViewApp({ - super.key, - this.shellBranding = ShellBranding.fallback, - }); + const ImWebViewApp({super.key, this.shellBranding = ShellBranding.fallback}); final ShellBranding shellBranding; @@ -137,9 +116,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { int _lineGeneration = 0; int _h5LineAttempt = 0; late ShellBranding _shellBranding; - Future? _cacheClearTask; - Timer? _domainExpiryTimer; - String _activeDomainDateKey = ''; _H5LineWebViewSlot get _currentSlot => _lineSlot; @@ -150,7 +126,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { _h5Line = AppConfig.h5Line; _shellBranding = widget.initialShellBranding; _lineSlot = _createLineSlot(_h5Line); - _scheduleDomainExpiryTimer(); unawaited(_initializeLines()); } @@ -171,17 +146,13 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel( 'OpenIMShell', - onMessageReceived: (message) => _handleShellMessage( - generation, - message, - ), + onMessageReceived: (message) => + _handleShellMessage(generation, message), ) ..addJavaScriptChannel( 'OpenIMFlutterShell', - onMessageReceived: (message) => _handleFlutterShellMessage( - generation, - message, - ), + onMessageReceived: (message) => + _handleFlutterShellMessage(generation, message), ) ..setBackgroundColor(Colors.white) ..setNavigationDelegate( @@ -252,14 +223,12 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { } try { - final result = await _androidFilePickerChannel.invokeListMethod( - 'pickFiles', - { - 'acceptTypes': params.acceptTypes, - 'allowMultiple': params.mode == FileSelectorMode.openMultiple, - 'capture': params.isCaptureEnabled, - }, - ); + final result = await _androidFilePickerChannel + .invokeListMethod('pickFiles', { + 'acceptTypes': params.acceptTypes, + 'allowMultiple': params.mode == FileSelectorMode.openMultiple, + 'capture': params.isCaptureEnabled, + }); return result ?? []; } catch (_) { return []; @@ -269,7 +238,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { @override void dispose() { WidgetsBinding.instance.removeObserver(this); - _domainExpiryTimer?.cancel(); _lineSlot.dispose(); super.dispose(); } @@ -277,12 +245,13 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { - unawaited(_refreshExpiredH5LinesIfNeeded()); + unawaited(_syncShellRequestUrl()); + unawaited(_syncShellBranding()); + unawaited(_syncKeyboardState()); } } Future _initializeLines() async { - await _clearInitialH5CachesIfNeeded(); await _prepareAndLoadH5Line(forceNew: false); } @@ -307,7 +276,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { _isPreparingInitialLine = false; }); await _loadCurrentLine(forceReload: true); - _scheduleDomainExpiryTimer(); return; } @@ -323,40 +291,25 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { _currentSlot.loadError = '当前线路暂不可用,请稍后重试'; _currentSlot.showShellCover = false; }); - _scheduleDomainExpiryTimer(); } - Future _loadCachedOrCreateH5Line({ - required bool forceNew, - }) async { - final today = _domainDateKey(DateTime.now()); - _activeDomainDateKey = today; - + Future _loadCachedOrCreateH5Line({required bool forceNew}) async { try { final preferences = await SharedPreferences.getInstance(); final cachedUrl = preferences.getString(_h5DomainCacheUrlKey); final cachedLine = cachedUrl == null ? null : AppConfig.h5LineFromUrl(cachedUrl); - final canUseCachedLine = !forceNew && - preferences.getString(_h5DomainCacheDateKey) == today && - preferences.getString(_h5DomainCacheSessionKey) == - _h5DomainProcessSession && - cachedLine != null; + final canUseCachedLine = !forceNew && cachedLine != null; if (canUseCachedLine) { - _logH5Line('复用本进程当天随机线路', cachedLine); + _logH5Line('复用已保存 H5 线路', cachedLine); return cachedLine; } final line = AppConfig.createRandomH5Line(attempt: _h5LineAttempt); _h5LineAttempt += 1; - await preferences.setString(_h5DomainCacheDateKey, today); - await preferences.setString( - _h5DomainCacheSessionKey, - _h5DomainProcessSession, - ); await preferences.setString(_h5DomainCacheUrlKey, line.url); - _logH5Line('生成当天随机线路', line); + _logH5Line('生成并保存 H5 线路', line); return line; } catch (_) { final line = AppConfig.createRandomH5Line(attempt: _h5LineAttempt); @@ -366,132 +319,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { } } - Future _refreshExpiredH5LinesIfNeeded() async { - final today = _domainDateKey(DateTime.now()); - if (_isPreparingInitialLine || _activeDomainDateKey == today) { - _scheduleDomainExpiryTimer(); - return; - } - - if (mounted) { - setState(() { - _isPreparingInitialLine = true; - }); - } - - await _clearAllH5Caches(); - await _prepareAndLoadH5Line(forceNew: true); - } - - void _scheduleDomainExpiryTimer() { - _domainExpiryTimer?.cancel(); - - final now = DateTime.now(); - final nextMidnight = DateTime(now.year, now.month, now.day + 1); - final delay = nextMidnight.difference(now) + const Duration(seconds: 1); - _domainExpiryTimer = Timer( - delay, - () => unawaited(_refreshExpiredH5LinesIfNeeded()), - ); - } - - Future _clearInitialH5CachesIfNeeded() async { - try { - final preferences = await SharedPreferences.getInstance(); - if (preferences.getBool(_initialH5CacheClearedKey) == true) { - return; - } - - await _clearAllH5Caches(); - await preferences.setBool(_initialH5CacheClearedKey, true); - } catch (_) { - await _clearAllH5Caches(); - } - } - - Future _clearAllH5Caches() { - final runningTask = _cacheClearTask; - if (runningTask != null) { - return runningTask; - } - - final task = _clearAllH5CachesInternal(); - _cacheClearTask = task; - return task.whenComplete(() { - if (identical(_cacheClearTask, task)) { - _cacheClearTask = null; - } - }); - } - - Future _clearAllH5CachesInternal() async { - await _clearNativeH5WebsiteData(); - await _clearCookiesSafely(); - await _clearPageStorage(_lineSlot.controller); - await _clearControllerStorage(_lineSlot.controller); - } - - Future _clearNativeH5WebsiteData() async { - try { - await _h5CacheChannel.invokeMethod('clearAllWebsiteData'); - } catch (_) { - // Older native shells may not expose the cache channel yet. - } - } - - Future _clearCookiesSafely() async { - try { - await WebViewCookieManager().clearCookies(); - } catch (_) { - // Cookie clearing is best-effort across platform WebView implementations. - } - } - - Future _clearControllerStorage(WebViewController controller) async { - try { - await controller.clearCache(); - } catch (_) {} - - try { - await controller.clearLocalStorage(); - } catch (_) {} - } - - Future _clearPageStorage(WebViewController controller) async { - const script = ''' -(() => { - try { - if ('caches' in window) { - caches.keys().then((keys) => Promise.all(keys.map((key) => caches.delete(key)))); - } - } catch (_) {} - try { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.getRegistrations() - .then((items) => Promise.all(items.map((item) => item.unregister()))); - } - } catch (_) {} - try { - if (typeof indexedDB !== 'undefined' && indexedDB.databases) { - indexedDB.databases().then((databases) => { - databases.forEach((database) => { - if (database && database.name) { - indexedDB.deleteDatabase(database.name); - } - }); - }); - } - } catch (_) {} - try { window.localStorage.clear(); } catch (_) {} - try { window.sessionStorage.clear(); } catch (_) {} -})(); -'''; - - try { - await controller.runJavaScript(script); - } catch (_) {} - } - Future _probeCurrentLine() async { final generation = _lineGeneration; final line = _lineSlot.line; @@ -907,7 +734,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { _isReplacingUnavailableLine = true; try { - await _clearAllH5Caches(); if (mounted) { setState(() { _isPreparingInitialLine = true; @@ -1050,9 +876,7 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { Positioned.fill( child: _ShellFallback(progress: currentSlot.progress), ), - Positioned.fill( - child: _buildWebViewWidget(), - ), + Positioned.fill(child: _buildWebViewWidget()), if (currentSlot.showShellCover && !currentSlot.hasPresentedFirstScreen) Positioned.fill( @@ -1107,10 +931,7 @@ class _NativeMediaPermissionResult { } class _H5LineWebViewSlot { - _H5LineWebViewSlot({ - required this.line, - required this.controller, - }); + _H5LineWebViewSlot({required this.line, required this.controller}); final H5Line line; final WebViewController controller; @@ -1159,10 +980,7 @@ class _ErrorPanel extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 20), - FilledButton( - onPressed: onRetry, - child: const Text('重新加载'), - ), + FilledButton(onPressed: onRetry, child: const Text('重新加载')), ], ), ), @@ -1188,8 +1006,10 @@ class _ShellFallback extends StatelessWidget { builder: (context, constraints) { final markSize = (constraints.maxWidth * 0.34).clamp(116.0, 148.0); final contentTop = constraints.maxHeight * 0.28; - final progressWidth = - (constraints.maxWidth * 0.32).clamp(116.0, 152.0); + final progressWidth = (constraints.maxWidth * 0.32).clamp( + 116.0, + 152.0, + ); return Align( alignment: Alignment.topCenter, @@ -1318,11 +1138,7 @@ class _H5LoadingBackgroundPainter extends CustomPainter { ..shader = const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - Color(0xFFEAF7FF), - Color(0xFFF8FCFF), - Colors.white, - ], + colors: [Color(0xFFEAF7FF), Color(0xFFF8FCFF), Colors.white], stops: [0, 0.52, 1], ).createShader(pageRect); canvas.drawRect(pageRect, pagePaint); @@ -1471,10 +1287,7 @@ class _BubbleCheckPainter extends CustomPainter { ..shader = const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ - Color(0xFFE8FBF8), - Color(0xFFAEECE4), - ], + colors: [Color(0xFFE8FBF8), Color(0xFFAEECE4)], ).createShader(backBubble.outerRect); canvas.drawRRect(backBubble, backBubblePaint); @@ -1499,11 +1312,7 @@ class _BubbleCheckPainter extends CustomPainter { ..shader = const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ - Color(0xFFA8DDFF), - _shellAccent, - _shellAccentDeep, - ], + colors: [Color(0xFFA8DDFF), _shellAccent, _shellAccentDeep], stops: [0, 0.48, 1], ).createShader(frontRect); canvas.drawPath(frontTail, bubblePaint);