125 lines
3.4 KiB
Dart
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),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|