diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 8f591ea..f53a421 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -83,6 +83,19 @@ class AppConfig { return url; } + static bool isLoginPageUrl(String? url) { + if (url == null || url.isEmpty) { + return false; + } + + final uri = Uri.tryParse(url); + if (uri == null) { + return false; + } + + return _isLoginPath(uri.path) || _isLoginPath(uri.fragment); + } + static int? lineIndexForUrl(String url) { final uri = Uri.tryParse(url); if (uri == null) { @@ -123,6 +136,21 @@ class AppConfig { return index; } + static bool _isLoginPath(String path) { + final normalized = path.trim(); + if (normalized.isEmpty) { + return false; + } + + final pathOnly = normalized.split('?').first.split('#').first; + final segments = pathOnly.split('/').where((segment) => segment.isNotEmpty); + if (segments.isEmpty) { + return false; + } + + return segments.last == 'login'; + } + static bool _isSameLine(Uri lineUri, Uri uri) { if (lineUri.host.toLowerCase() != uri.host.toLowerCase()) { return false; diff --git a/lib/main.dart b/lib/main.dart index f96f9e2..7890c3a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -133,11 +133,12 @@ class _H5ShellPageState extends State { setState(() => _lineSlots[lineIndex].progress = progress); } }, - onPageStarted: (_) { + onPageStarted: (url) { final slot = _lineSlots[lineIndex]; slot.shellCoverFallbackTimer?.cancel(); if (mounted) { setState(() { + slot.setCurrentUrl(url); slot.loadError = null; slot.progress = 0; slot.showShellCover = true; @@ -147,6 +148,9 @@ class _H5ShellPageState extends State { onPageFinished: (_) { unawaited(_handlePageFinished(lineIndex)); }, + onUrlChange: (change) { + _updateSlotUrl(lineIndex, change.url); + }, onWebResourceError: (error) { if (error.isForMainFrame ?? true) { final slot = _lineSlots[lineIndex]; @@ -183,6 +187,7 @@ class _H5ShellPageState extends State { Future _handlePageFinished(int lineIndex) async { await _loadShellBrandingIfNeeded(); await _syncShellBranding(lineIndex); + await _installRouteObserver(lineIndex); if (mounted) { setState(() { _lineSlots[lineIndex].progress = 100; @@ -196,6 +201,8 @@ class _H5ShellPageState extends State { final decoded = jsonDecode(message.message); if (decoded is Map && decoded['type'] == 'first-screen-ready') { _hideShellCover(lineIndex); + } else if (decoded is Map && decoded['type'] == 'route-changed') { + _updateSlotUrl(lineIndex, decoded['url']?.toString()); } } catch (_) { if (message.message == 'first-screen-ready') { @@ -260,6 +267,60 @@ class _H5ShellPageState extends State { return _runJavaScriptSafely(lineIndex, script); } + Future _installRouteObserver(int lineIndex) { + const script = ''' +(() => { + try { + const notify = () => { + try { + window.OpenIMShell.postMessage(JSON.stringify({ + type: 'route-changed', + url: window.location.href + })); + } catch (_) {} + }; + + if (window.__OPENIM_FLUTTER_ROUTE_OBSERVER__) { + window.__OPENIM_FLUTTER_ROUTE_OBSERVER__.notify(); + return; + } + + const originalPushState = window.history.pushState; + const originalReplaceState = window.history.replaceState; + + window.history.pushState = function() { + const result = originalPushState.apply(this, arguments); + notify(); + return result; + }; + + window.history.replaceState = function() { + const result = originalReplaceState.apply(this, arguments); + notify(); + return result; + }; + + window.addEventListener('popstate', notify); + window.__OPENIM_FLUTTER_ROUTE_OBSERVER__ = { notify }; + notify(); + } catch (_) {} +})(); +'''; + return _runJavaScriptSafely(lineIndex, script); + } + + void _updateSlotUrl(int lineIndex, String? url) { + if (url == null || lineIndex < 0 || lineIndex >= _lineSlots.length) { + return; + } + + final slot = _lineSlots[lineIndex]; + final changed = slot.setCurrentUrl(url); + if (changed && mounted) { + setState(() {}); + } + } + Future _ensureLineLoaded(int lineIndex) async { final slot = _lineSlots[lineIndex]; if (slot.hasLoadedInitialRequest) { @@ -270,6 +331,7 @@ class _H5ShellPageState extends State { if (mounted) { slot.shellCoverFallbackTimer?.cancel(); setState(() { + slot.setCurrentUrl(slot.line.url); slot.loadError = null; slot.progress = 0; slot.showShellCover = true; @@ -369,6 +431,7 @@ class _H5ShellPageState extends State { final bottomInset = MediaQuery.viewInsetsOf(context).bottom; final showLineSwitch = !currentSlot.showShellCover && currentSlot.loadError == null && + currentSlot.isLoginPage && bottomInset == 0; return PopScope( @@ -448,8 +511,21 @@ class _H5LineWebViewSlot { String? loadError; bool showShellCover = true; bool hasLoadedInitialRequest = false; + bool isLoginPage = false; + String? currentUrl; Timer? shellCoverFallbackTimer; + bool setCurrentUrl(String url) { + final nextIsLoginPage = AppConfig.isLoginPageUrl(url); + if (currentUrl == url && isLoginPage == nextIsLoginPage) { + return false; + } + + currentUrl = url; + isLoginPage = nextIsLoginPage; + return true; + } + void dispose() { shellCoverFallbackTimer?.cancel(); } diff --git a/test/widget_test.dart b/test/widget_test.dart index c44fdbf..0914530 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -50,6 +50,20 @@ void main() { expect(AppConfig.lineIndexForUrl('${lineUrl}login'), 0); }); + test('detects only H5 login routes for shell line switch display', () { + expect( + AppConfig.isLoginPageUrl('https://h5-test.imharry.work/login'), isTrue); + expect(AppConfig.isLoginPageUrl('https://h5-test.imharry.work/app/login'), + isTrue); + expect(AppConfig.isLoginPageUrl('https://h5-test.imharry.work/#/login'), + isTrue); + expect(AppConfig.isLoginPageUrl('https://h5-test.imharry.work/'), isFalse); + expect(AppConfig.isLoginPageUrl('https://h5-test.imharry.work/contact'), + isFalse); + expect(AppConfig.isLoginPageUrl('https://h5-test.imharry.work/getCode'), + isFalse); + }); + test('refreshes an H5 route URL without adding branding to the URL', () { final uri = Uri.parse( AppConfig.withFreshShellParams(