Files
ingtrakapp/lib/screens/bank_auth_screen.dart
2026-09-06 21:13:30 +02:00

125 lines
3.4 KiB
Dart

/// 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<BankAuthScreen> createState() => _BankAuthScreenState();
}
class _BankAuthScreenState extends State<BankAuthScreen> {
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),
),
],
),
);
}
}