aslan-flutter/lib/chatvibe_features/webview/at_webview_navigation_handler.dart
2026-07-01 18:25:58 +08:00

204 lines
5.4 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:aslan/chatvibe_ui/components/at_tts.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';
class ATWebViewNavigationHandler {
static const MethodChannel _externalIntentChannel = MethodChannel(
'com.chat.auu.webview/external_intent',
);
static const Set<String> _webSchemes = {
'about',
'blob',
'data',
'file',
'http',
'https',
'javascript',
};
static Future<NavigationDecision> handleNavigationRequest(
NavigationRequest request,
) async {
final uri = Uri.tryParse(request.url);
if (uri == null) return NavigationDecision.prevent;
final scheme = uri.scheme.toLowerCase();
if (scheme.isEmpty || _webSchemes.contains(scheme)) {
return NavigationDecision.navigate;
}
final launched = await launchExternal(uri);
if (!launched) {
showOpenAppFailedTip(uri);
}
return NavigationDecision.prevent;
}
static Future<bool> handleWebResourceError(WebResourceError error) async {
final errorUrl = error.url;
if (errorUrl == null || errorUrl.isEmpty) return false;
final uri = Uri.tryParse(errorUrl);
if (uri == null) return false;
final scheme = uri.scheme.toLowerCase();
if (scheme.isEmpty || _webSchemes.contains(scheme)) {
return false;
}
final launched = await launchExternal(uri);
if (!launched) {
showOpenAppFailedTip(uri);
}
return launched;
}
static Future<bool> launchExternal(Uri uri) async {
for (final targetUri in [uri, ..._fallbackUris(uri)]) {
if (await _tryLaunch(targetUri)) {
return true;
}
}
return false;
}
static Future<bool> _tryLaunch(Uri uri) async {
if (await _tryLaunchNative(uri)) {
return true;
}
if (defaultTargetPlatform == TargetPlatform.android && _isPaymentUri(uri)) {
return false;
}
try {
return await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (e) {
debugPrint('Open external app failed: $uri, error: $e');
return false;
}
}
static Future<bool> _tryLaunchNative(Uri uri) async {
if (defaultTargetPlatform != TargetPlatform.android) {
return false;
}
try {
final result = await _externalIntentChannel.invokeMethod<bool>(
'openExternalUrl',
uri.toString(),
);
return result ?? false;
} on MissingPluginException {
return false;
} catch (e) {
debugPrint('Open native external app failed: $uri, error: $e');
return false;
}
}
static List<Uri> _fallbackUris(Uri uri) {
final fallbackUris = <Uri>[];
final browserFallbackUrl = _intentFallbackUrl(uri);
if (browserFallbackUrl != null) {
fallbackUris.add(browserFallbackUrl);
}
final upiFallbackUrl = _upiFallbackUrl(uri);
if (upiFallbackUrl != null) {
fallbackUris.add(upiFallbackUrl);
}
fallbackUris.addAll(_paymentAppSchemeFallbackUrls(upiFallbackUrl ?? uri));
fallbackUris.addAll(_upiIntentFallbackUrls(upiFallbackUrl ?? uri));
return fallbackUris;
}
static Uri? _intentFallbackUrl(Uri uri) {
if (uri.scheme.toLowerCase() != 'intent') return null;
final fallback = RegExp(
r'S\.browser_fallback_url=([^;]+)',
).firstMatch(uri.toString())?.group(1);
if (fallback == null || fallback.isEmpty) return null;
return Uri.tryParse(Uri.decodeComponent(fallback));
}
static Uri? _upiFallbackUrl(Uri uri) {
final scheme = uri.scheme.toLowerCase();
if (scheme != 'phonepe' &&
scheme != 'paytmmp' &&
scheme != 'bhim' &&
scheme != 'bhimupi') {
return null;
}
final path = uri.path.isNotEmpty ? uri.path : uri.host;
if (!path.split('/').contains('pay')) return null;
return uri.replace(scheme: 'upi', host: 'pay', path: '');
}
static Iterable<Uri> _paymentAppSchemeFallbackUrls(Uri uri) sync* {
if (uri.scheme.toLowerCase() != 'upi') return;
final path = uri.path.isNotEmpty ? uri.path : uri.host;
if (path != 'pay') return;
const schemes = ['phonepe', 'paytmmp', 'bhim'];
for (final scheme in schemes) {
yield uri.replace(scheme: scheme, host: 'pay', path: '');
}
}
static Iterable<Uri> _upiIntentFallbackUrls(Uri uri) sync* {
if (!_isPaymentUri(uri)) return;
final upiUri =
uri.scheme.toLowerCase() == 'upi' ? uri : _upiFallbackUrl(uri);
if (upiUri == null) return;
final path = upiUri.path.isNotEmpty ? upiUri.path : upiUri.host;
if (path != 'pay') return;
const packageNames = [
'com.phonepe.app',
'net.one97.paytm',
'com.google.android.apps.nbu.paisa.user',
'in.org.npci.upiapp',
];
final query = upiUri.hasQuery ? '?${upiUri.query}' : '';
for (final packageName in packageNames) {
yield Uri.parse(
'intent://pay$query#Intent;scheme=upi;package=$packageName;end',
);
}
}
static bool _isPaymentUri(Uri uri) {
final scheme = uri.scheme.toLowerCase();
return scheme == 'phonepe' ||
scheme == 'paytmmp' ||
scheme == 'bhim' ||
scheme == 'bhimupi' ||
scheme == 'upi' ||
scheme == 'intent';
}
static void showOpenAppFailedTip(Uri uri) {
ATTts.show(
_isPaymentUri(uri)
? 'Please install or set up the payment app first.'
: 'App is not installed.',
);
}
}