/// In-app browser for Enable Banking hosted auth. /// /// Enable Banking only allows http(s) redirect URLs, so we keep /// `http://localhost:8000/api/connect/callback` and capture `code` here /// when the WebView navigates to that callback. library; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; /// Result popped from [BankAuthScreen] after bank login. class BankAuthResult { const BankAuthResult({this.code, this.state, this.error}); final String? code; final String? state; final String? error; bool get hasCode => code != null && code!.isNotEmpty; } class BankAuthScreen extends StatefulWidget { const BankAuthScreen({ super.key, required this.authUrl, this.pendingState, }); final String authUrl; final String? pendingState; @override State createState() => _BankAuthScreenState(); } class _BankAuthScreenState extends State { late final WebViewController _controller; var _finishing = false; var _loading = true; @override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onPageStarted: (url) { if (mounted) setState(() => _loading = true); _handleUrl(url); }, onPageFinished: (_) { if (mounted) setState(() => _loading = false); }, onNavigationRequest: (request) { if (_handleUrl(request.url)) { return NavigationDecision.prevent; } return NavigationDecision.navigate; }, onWebResourceError: (error) { // Callback to localhost often fails to load on emulators; if we // already captured the code, ignore the load error. if (_finishing) return; debugPrint('Bank auth WebView error: ${error.description}'); }, ), ) ..loadRequest(Uri.parse(widget.authUrl)); } /// Returns true when [url] is the Enable Banking redirect callback. bool _handleUrl(String url) { final uri = Uri.tryParse(url); if (uri == null || _finishing) return false; if (!_isConnectCallback(uri)) return false; _finishing = true; final error = uri.queryParameters['error']; final code = uri.queryParameters['code']; final state = uri.queryParameters['state'] ?? widget.pendingState; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; Navigator.of(context).pop( BankAuthResult( code: code, state: state, error: error, ), ); }); return true; } bool _isConnectCallback(Uri uri) { final path = uri.path; return path == '/api/connect/callback' || path.endsWith('/api/connect/callback'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Connect bank'), leading: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), ), body: Stack( children: [ WebViewWidget(controller: _controller), if (_loading) const Align( alignment: Alignment.topCenter, child: LinearProgressIndicator(minHeight: 2), ), ], ), ); } }