feat: 优化 H5 URL 生成逻辑,移除品牌参数;更新测试用例以反映更改
This commit is contained in:
@@ -21,39 +21,25 @@ class AppConfig {
|
|||||||
final host = environmentHosts.isNotEmpty
|
final host = environmentHosts.isNotEmpty
|
||||||
? environmentHosts.first
|
? environmentHosts.first
|
||||||
: 'h5-im.imharry.work';
|
: 'h5-im.imharry.work';
|
||||||
return withFreshShellParams(
|
return withFreshShellParams(_normalizeHomeUrl(host));
|
||||||
_normalizeHomeUrl(host),
|
|
||||||
appName: appName,
|
|
||||||
appLogo: appLogo,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static String withFreshShellParams(
|
static String withFreshShellParams(String url) {
|
||||||
String url, {
|
|
||||||
String? appName,
|
|
||||||
String? appLogo,
|
|
||||||
}) {
|
|
||||||
final uri = Uri.parse(url);
|
final uri = Uri.parse(url);
|
||||||
final queryParameters = Map<String, String>.from(uri.queryParameters)
|
final queryParameters = Map<String, String>.from(uri.queryParameters)
|
||||||
..['flutter_shell'] = '1'
|
..['flutter_shell'] = '1'
|
||||||
..['shell_cache_bust'] = DateTime.now().millisecondsSinceEpoch.toString();
|
..['shell_cache_bust'] = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
queryParameters.remove('shell_app_name');
|
||||||
|
queryParameters.remove('shell_app_logo');
|
||||||
final fragmentParameters = Uri.splitQueryString(uri.fragment);
|
final fragmentParameters = Uri.splitQueryString(uri.fragment);
|
||||||
|
fragmentParameters.remove('shell_app_name');
|
||||||
final trimmedName = (appName ?? AppConfig.appName).trim();
|
fragmentParameters.remove('shell_app_logo');
|
||||||
if (trimmedName.isNotEmpty) {
|
|
||||||
queryParameters['shell_app_name'] = trimmedName;
|
|
||||||
}
|
|
||||||
|
|
||||||
final trimmedLogo = (appLogo ?? AppConfig.appLogo).trim();
|
|
||||||
if (trimmedLogo.isNotEmpty) {
|
|
||||||
fragmentParameters['shell_app_logo'] = trimmedLogo;
|
|
||||||
}
|
|
||||||
|
|
||||||
return uri
|
return uri
|
||||||
.replace(
|
.replace(
|
||||||
queryParameters: queryParameters,
|
queryParameters: queryParameters,
|
||||||
fragment: fragmentParameters.isEmpty
|
fragment: fragmentParameters.isEmpty
|
||||||
? null
|
? ''
|
||||||
: Uri(queryParameters: fragmentParameters).query,
|
: Uri(queryParameters: fragmentParameters).query,
|
||||||
)
|
)
|
||||||
.toString();
|
.toString();
|
||||||
|
|||||||
381
lib/main.dart
381
lib/main.dart
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
@@ -16,8 +15,6 @@ const _shellBackground = Color(0xFFF8FBFF);
|
|||||||
const _shellAccent = Color(0xFF0089FF);
|
const _shellAccent = Color(0xFF0089FF);
|
||||||
const _shellSubText = Color(0xFF8E9AB0);
|
const _shellSubText = Color(0xFF8E9AB0);
|
||||||
const _resumeCoverDuration = Duration(milliseconds: 700);
|
const _resumeCoverDuration = Duration(milliseconds: 700);
|
||||||
const _urlOnlyShellMode = true;
|
|
||||||
const _urlOnlyWebViewUrl = 'https://h5-im.imharry.work/';
|
|
||||||
const _noCacheHeaders = {
|
const _noCacheHeaders = {
|
||||||
'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
|
'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
|
||||||
'Pragma': 'no-cache',
|
'Pragma': 'no-cache',
|
||||||
@@ -27,87 +24,6 @@ const _shellBrandingChannel =
|
|||||||
MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
|
MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
|
||||||
const _shellWebViewCacheChannel =
|
const _shellWebViewCacheChannel =
|
||||||
MethodChannel('io.openim.flutter.im_webview_app/webview_cache');
|
MethodChannel('io.openim.flutter.im_webview_app/webview_cache');
|
||||||
const _runtimeCachePurgeFallbackDuration = Duration(seconds: 2);
|
|
||||||
const _purgeWebRuntimeCacheScript = r'''
|
|
||||||
(() => {
|
|
||||||
if (window.__openimShellRuntimeCachePurgeStarted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.__openimShellRuntimeCachePurgeStarted = true;
|
|
||||||
|
|
||||||
const targetUrl = () => {
|
|
||||||
try {
|
|
||||||
const target = new URL(window.location.href);
|
|
||||||
target.searchParams.set('shell_cache_bust', Date.now().toString());
|
|
||||||
target.searchParams.set('shell_runtime_cache_purged', '1');
|
|
||||||
return target.toString();
|
|
||||||
} catch (_) {
|
|
||||||
return window.location.href;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const notifyFlutter = (payload) => {
|
|
||||||
const channel = window.OpenIMFlutterShell;
|
|
||||||
if (channel && typeof channel.postMessage === 'function') {
|
|
||||||
channel.postMessage(JSON.stringify(payload));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const purgeRuntimeCache = async () => {
|
|
||||||
const result = {
|
|
||||||
cacheStorageKeys: 0,
|
|
||||||
serviceWorkerRegistrations: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ('caches' in window && caches.keys) {
|
|
||||||
const keys = await caches.keys();
|
|
||||||
result.cacheStorageKeys = keys.length;
|
|
||||||
await Promise.all(keys.map((key) => caches.delete(key)));
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
navigator.serviceWorker &&
|
|
||||||
typeof navigator.serviceWorker.getRegistrations === 'function'
|
|
||||||
) {
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
||||||
result.serviceWorkerRegistrations = registrations.length;
|
|
||||||
await Promise.all(
|
|
||||||
registrations.map((registration) => registration.unregister())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
purgeRuntimeCache()
|
|
||||||
.then((result) => {
|
|
||||||
const payload = {
|
|
||||||
type: 'runtimeCachePurged',
|
|
||||||
url: targetUrl(),
|
|
||||||
result,
|
|
||||||
};
|
|
||||||
if (!notifyFlutter(payload)) {
|
|
||||||
window.location.replace(payload.url);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
const payload = {
|
|
||||||
type: 'runtimeCachePurged',
|
|
||||||
url: targetUrl(),
|
|
||||||
};
|
|
||||||
if (!notifyFlutter(payload)) {
|
|
||||||
window.location.replace(payload.url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
''';
|
|
||||||
const _inspectH5SnapshotScript = r'''
|
const _inspectH5SnapshotScript = r'''
|
||||||
(() => {
|
(() => {
|
||||||
const toAbsoluteUrl = (value) => {
|
const toAbsoluteUrl = (value) => {
|
||||||
@@ -265,10 +181,6 @@ Future<void> main() async {
|
|||||||
systemNavigationBarIconBrightness: Brightness.dark,
|
systemNavigationBarIconBrightness: Brightness.dark,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (_urlOnlyShellMode) {
|
|
||||||
runApp(const UrlOnlyWebViewApp());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final shellBranding = await ShellBranding.load();
|
final shellBranding = await ShellBranding.load();
|
||||||
runApp(ImWebViewApp(shellBranding: shellBranding));
|
runApp(ImWebViewApp(shellBranding: shellBranding));
|
||||||
}
|
}
|
||||||
@@ -306,121 +218,6 @@ class ShellBranding {
|
|||||||
static String _trim(String? value) => value?.trim() ?? '';
|
static String _trim(String? value) => value?.trim() ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
class UrlOnlyWebViewApp extends StatelessWidget {
|
|
||||||
const UrlOnlyWebViewApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
title: _urlOnlyWebViewUrl,
|
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
theme: ThemeData(scaffoldBackgroundColor: Colors.white),
|
|
||||||
home: const UrlOnlyWebViewPage(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UrlOnlyWebViewPage extends StatefulWidget {
|
|
||||||
const UrlOnlyWebViewPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<UrlOnlyWebViewPage> createState() => _UrlOnlyWebViewPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _UrlOnlyWebViewPageState extends State<UrlOnlyWebViewPage> {
|
|
||||||
late final WebViewController _controller;
|
|
||||||
|
|
||||||
int _progress = 0;
|
|
||||||
String? _loadError;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = WebViewController()
|
|
||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
|
||||||
..setBackgroundColor(Colors.white)
|
|
||||||
..setNavigationDelegate(
|
|
||||||
NavigationDelegate(
|
|
||||||
onProgress: (progress) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => _progress = progress);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPageStarted: (_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_loadError = null;
|
|
||||||
_progress = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPageFinished: (url) {
|
|
||||||
debugPrint('[UrlOnlyWebView] finished: $url');
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => _progress = 100);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onWebResourceError: (error) {
|
|
||||||
if (error.isForMainFrame ?? true) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => _loadError = error.description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onNavigationRequest: _handleUrlOnlyNavigationRequest,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
..loadRequest(Uri.parse(_urlOnlyWebViewUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<NavigationDecision> _handleUrlOnlyNavigationRequest(
|
|
||||||
NavigationRequest request,
|
|
||||||
) async {
|
|
||||||
final uri = Uri.tryParse(request.url);
|
|
||||||
if (uri == null) {
|
|
||||||
return NavigationDecision.prevent;
|
|
||||||
}
|
|
||||||
|
|
||||||
const webSchemes = {'http', 'https', 'about', 'data'};
|
|
||||||
if (webSchemes.contains(uri.scheme)) {
|
|
||||||
return NavigationDecision.navigate;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
} catch (_) {}
|
|
||||||
return NavigationDecision.prevent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
body: SafeArea(
|
|
||||||
bottom: false,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
WebViewWidget(controller: _controller),
|
|
||||||
if (_progress < 100)
|
|
||||||
LinearProgressIndicator(
|
|
||||||
value: _progress == 0 ? null : _progress / 100,
|
|
||||||
minHeight: 2,
|
|
||||||
),
|
|
||||||
if (_loadError != null)
|
|
||||||
_ErrorPanel(
|
|
||||||
message: _loadError!,
|
|
||||||
onRetry: () {
|
|
||||||
setState(() => _loadError = null);
|
|
||||||
_controller.loadRequest(Uri.parse(_urlOnlyWebViewUrl));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImWebViewApp extends StatelessWidget {
|
class ImWebViewApp extends StatelessWidget {
|
||||||
const ImWebViewApp({
|
const ImWebViewApp({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -459,11 +256,7 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
int _progress = 0;
|
int _progress = 0;
|
||||||
String? _loadError;
|
String? _loadError;
|
||||||
bool _showShellCover = true;
|
bool _showShellCover = true;
|
||||||
bool _runtimeCachePurgeStarted = false;
|
|
||||||
bool _runtimeCachePurgeReloaded = false;
|
|
||||||
String? _h5SnapshotDebugText;
|
|
||||||
Timer? _shellCoverTimer;
|
Timer? _shellCoverTimer;
|
||||||
Timer? _runtimeCachePurgeFallbackTimer;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -476,7 +269,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_shellCoverTimer?.cancel();
|
_shellCoverTimer?.cancel();
|
||||||
_runtimeCachePurgeFallbackTimer?.cancel();
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -596,10 +388,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
if (type == 'openAppSettings') {
|
if (type == 'openAppSettings') {
|
||||||
unawaited(openAppSettings());
|
unawaited(openAppSettings());
|
||||||
}
|
}
|
||||||
if (type == 'runtimeCachePurged' && payload != null) {
|
|
||||||
debugPrint('[H5Shell] runtime cache purged: ${jsonEncode(payload)}');
|
|
||||||
unawaited(_reloadAfterRuntimeCachePurge(payload['url'] as String?));
|
|
||||||
}
|
|
||||||
if (type == 'requestMediaPermissions' && payload != null) {
|
if (type == 'requestMediaPermissions' && payload != null) {
|
||||||
unawaited(_handleMediaPermissionBridgeRequest(payload));
|
unawaited(_handleMediaPermissionBridgeRequest(payload));
|
||||||
}
|
}
|
||||||
@@ -665,37 +453,11 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _freshHomeUrl() {
|
String _freshHomeUrl() {
|
||||||
return AppConfig.homeUrl(
|
return AppConfig.homeUrl();
|
||||||
appName: widget.shellBranding.appName,
|
|
||||||
appLogo: widget.shellBranding.appLogo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _freshShellUrl(String? url) {
|
|
||||||
final value = url?.trim();
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return _freshHomeUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return AppConfig.withFreshShellParams(
|
|
||||||
value,
|
|
||||||
appName: widget.shellBranding.appName,
|
|
||||||
appLogo: widget.shellBranding.appLogo,
|
|
||||||
);
|
|
||||||
} catch (_) {
|
|
||||||
return _freshHomeUrl();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handlePageFinished() async {
|
Future<void> _handlePageFinished() async {
|
||||||
final waitingForRuntimeCacheReload =
|
await _syncShellBranding();
|
||||||
await _purgeWebRuntimeCacheAndReloadIfNeeded();
|
|
||||||
if (waitingForRuntimeCacheReload) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
unawaited(_syncShellBranding());
|
|
||||||
unawaited(_injectShellMediaPermissionBridge());
|
unawaited(_injectShellMediaPermissionBridge());
|
||||||
unawaited(_logLoadedH5Snapshot());
|
unawaited(_logLoadedH5Snapshot());
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -706,56 +468,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _purgeWebRuntimeCacheAndReloadIfNeeded() async {
|
|
||||||
if (_runtimeCachePurgeStarted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_runtimeCachePurgeStarted = true;
|
|
||||||
try {
|
|
||||||
await _controller.runJavaScript(_purgeWebRuntimeCacheScript);
|
|
||||||
} catch (_) {
|
|
||||||
_runtimeCachePurgeStarted = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_runtimeCachePurgeFallbackTimer?.cancel();
|
|
||||||
_runtimeCachePurgeFallbackTimer = Timer(
|
|
||||||
_runtimeCachePurgeFallbackDuration,
|
|
||||||
() {
|
|
||||||
if (!_runtimeCachePurgeReloaded) {
|
|
||||||
unawaited(_reloadAfterRuntimeCachePurge(null));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _reloadAfterRuntimeCachePurge(String? url) async {
|
|
||||||
if (_runtimeCachePurgeReloaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_runtimeCachePurgeReloaded = true;
|
|
||||||
_runtimeCachePurgeFallbackTimer?.cancel();
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_loadError = null;
|
|
||||||
_progress = 0;
|
|
||||||
_showShellCover = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
String? currentUrl;
|
|
||||||
if (url == null || url.trim().isEmpty) {
|
|
||||||
try {
|
|
||||||
currentUrl = await _controller.currentUrl();
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _loadUrl(_freshShellUrl(url ?? currentUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _prepareWebViewForFreshLoad() async {
|
Future<void> _prepareWebViewForFreshLoad() async {
|
||||||
await _configureAndroidNoCache();
|
await _configureAndroidNoCache();
|
||||||
try {
|
try {
|
||||||
@@ -815,61 +527,11 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'[H5Shell] loaded H5 snapshot: $snapshot',
|
'[H5Shell] loaded H5 snapshot: $snapshot',
|
||||||
);
|
);
|
||||||
_updateH5SnapshotDebugText(snapshot);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint('[H5Shell] loaded H5 snapshot failed: $error');
|
debugPrint('[H5Shell] loaded H5 snapshot failed: $error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateH5SnapshotDebugText(String snapshot) {
|
|
||||||
if (!kDebugMode || !mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_h5SnapshotDebugText = _formatH5SnapshotDebugText(snapshot);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatH5SnapshotDebugText(String snapshot) {
|
|
||||||
try {
|
|
||||||
final decoded = jsonDecode(snapshot);
|
|
||||||
if (decoded is! Map) {
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
final href = (decoded['href'] as String? ?? '').trim();
|
|
||||||
final title = (decoded['title'] as String? ?? '').trim();
|
|
||||||
final bodyText = (decoded['bodyText'] as String? ?? '').trim();
|
|
||||||
final scripts = decoded['scripts'];
|
|
||||||
final links = decoded['links'];
|
|
||||||
final assets = <String>[
|
|
||||||
if (scripts is List)
|
|
||||||
...scripts.whereType<String>().where((value) => value.isNotEmpty),
|
|
||||||
if (links is List)
|
|
||||||
...links.whereType<String>().where((value) => value.isNotEmpty),
|
|
||||||
];
|
|
||||||
final importantAssets = assets
|
|
||||||
.where(
|
|
||||||
(asset) =>
|
|
||||||
asset.contains('/assets/index-') ||
|
|
||||||
asset.contains('LoginBrandHeader') ||
|
|
||||||
asset.contains('useLoginBranding'),
|
|
||||||
)
|
|
||||||
.take(8)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
return [
|
|
||||||
if (title.isNotEmpty) 'title: $title',
|
|
||||||
if (href.isNotEmpty) 'url: $href',
|
|
||||||
if (importantAssets.isNotEmpty) 'assets:\n$importantAssets',
|
|
||||||
if (bodyText.isNotEmpty) 'text: $bodyText',
|
|
||||||
].join('\n');
|
|
||||||
} catch (_) {
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _decodeJavaScriptStringResult(Object? result) {
|
String _decodeJavaScriptStringResult(Object? result) {
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return '';
|
return '';
|
||||||
@@ -1038,15 +700,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
message: _loadError!,
|
message: _loadError!,
|
||||||
onRetry: () => unawaited(_loadHome()),
|
onRetry: () => unawaited(_loadHome()),
|
||||||
),
|
),
|
||||||
if (kDebugMode && _h5SnapshotDebugText != null)
|
|
||||||
Positioned(
|
|
||||||
left: 8,
|
|
||||||
right: 8,
|
|
||||||
bottom: 12,
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: _H5SnapshotDebugPanel(text: _h5SnapshotDebugText!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1055,36 +708,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _H5SnapshotDebugPanel extends StatelessWidget {
|
|
||||||
const _H5SnapshotDebugPanel({required this.text});
|
|
||||||
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withValues(alpha: 0.72),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
maxLines: 10,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 10,
|
|
||||||
height: 1.25,
|
|
||||||
letterSpacing: 0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ErrorPanel extends StatelessWidget {
|
class _ErrorPanel extends StatelessWidget {
|
||||||
const _ErrorPanel({required this.message, required this.onRetry});
|
const _ErrorPanel({required this.message, required this.onRetry});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('loads the configured H5 root URL with shell branding', () {
|
test('loads the configured H5 root URL with shell launch params', () {
|
||||||
const logo = 'data:image/png;base64,abc+/=';
|
const logo = 'data:image/png;base64,abc+/=';
|
||||||
|
|
||||||
final uri = Uri.parse(
|
final uri = Uri.parse(
|
||||||
@@ -22,29 +22,29 @@ void main() {
|
|||||||
expect(uri.host, 'h5-im.imharry.work');
|
expect(uri.host, 'h5-im.imharry.work');
|
||||||
expect(uri.path, '/');
|
expect(uri.path, '/');
|
||||||
expect(uri.queryParameters['flutter_shell'], '1');
|
expect(uri.queryParameters['flutter_shell'], '1');
|
||||||
expect(uri.queryParameters['shell_app_name'], 'Shell Test');
|
expect(uri.queryParameters.containsKey('shell_app_name'), isFalse);
|
||||||
expect(uri.queryParameters.containsKey('shell_app_logo'), isFalse);
|
expect(uri.queryParameters.containsKey('shell_app_logo'), isFalse);
|
||||||
expect(uri.queryParameters['shell_cache_bust'], isNotEmpty);
|
expect(uri.queryParameters['shell_cache_bust'], isNotEmpty);
|
||||||
expect(uri.toString(), isNot(contains('%25')));
|
expect(uri.toString(), isNot(contains('%25')));
|
||||||
expect(Uri.splitQueryString(uri.fragment)['shell_app_logo'], logo);
|
expect(Uri.splitQueryString(uri.fragment).containsKey('shell_app_logo'),
|
||||||
|
isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keeps shell branding when refreshing an H5 route URL', () {
|
test('refreshes an H5 route URL without adding branding to the URL', () {
|
||||||
const logo = 'data:image/png;base64,routeLogo+/=';
|
|
||||||
|
|
||||||
final uri = Uri.parse(
|
final uri = Uri.parse(
|
||||||
AppConfig.withFreshShellParams(
|
AppConfig.withFreshShellParams(
|
||||||
'https://h5-im.imharry.work/login?from=runtime',
|
'https://h5-im.imharry.work/login?from=runtime&shell_app_name=Old'
|
||||||
appName: 'Shell Route',
|
'#shell_app_logo=old',
|
||||||
appLogo: logo,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(uri.path, '/login');
|
expect(uri.path, '/login');
|
||||||
expect(uri.queryParameters['from'], 'runtime');
|
expect(uri.queryParameters['from'], 'runtime');
|
||||||
expect(uri.queryParameters['flutter_shell'], '1');
|
expect(uri.queryParameters['flutter_shell'], '1');
|
||||||
expect(uri.queryParameters['shell_app_name'], 'Shell Route');
|
expect(uri.queryParameters.containsKey('shell_app_name'), isFalse);
|
||||||
|
expect(uri.queryParameters.containsKey('shell_app_logo'), isFalse);
|
||||||
expect(uri.queryParameters['shell_cache_bust'], isNotEmpty);
|
expect(uri.queryParameters['shell_cache_bust'], isNotEmpty);
|
||||||
expect(Uri.splitQueryString(uri.fragment)['shell_app_logo'], logo);
|
expect(Uri.splitQueryString(uri.fragment).containsKey('shell_app_logo'),
|
||||||
|
isFalse);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user