181 lines
5.3 KiB
Dart
181 lines
5.3 KiB
Dart
/// [ChangeNotifier] for bank link status, balance, and transactions.
|
|
library;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import '../models/bank.dart';
|
|
import '../services/ingtrak_api.dart';
|
|
import '../utils/constants.dart';
|
|
|
|
/// App-wide banking state backed by the local ingtrak API.
|
|
class BankController extends ChangeNotifier {
|
|
BankController({IngtrakApi? api}) : _api = api ?? IngtrakApi();
|
|
|
|
final IngtrakApi _api;
|
|
|
|
ConnectStatus _status = ConnectStatus.disconnected;
|
|
BankBalance? _balance;
|
|
List<BankTransaction> _transactions = const [];
|
|
|
|
bool _isLoading = false;
|
|
bool _isConnecting = false;
|
|
String? _error;
|
|
String? _pendingState;
|
|
|
|
ConnectStatus get status => _status;
|
|
BankBalance? get balance => _balance;
|
|
List<BankTransaction> get transactions => List.unmodifiable(_transactions);
|
|
bool get isLoading => _isLoading;
|
|
bool get isConnecting => _isConnecting;
|
|
String? get error => _error;
|
|
bool get isLinked => _status.linked;
|
|
String get apiBaseUrl => _api.baseUrl;
|
|
String? get pendingState => _pendingState;
|
|
|
|
/// Load status; if linked, also load balance + transactions.
|
|
Future<void> refresh() async {
|
|
_isLoading = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_status = await _api.fetchStatus();
|
|
if (_status.linked) {
|
|
await _loadLinkedData();
|
|
} else {
|
|
_balance = null;
|
|
_transactions = const [];
|
|
}
|
|
} on IngtrakApiException catch (error, stackTrace) {
|
|
_error = error.message;
|
|
debugPrint('BankController.refresh failed: $error\n$stackTrace');
|
|
} catch (error, stackTrace) {
|
|
_error = 'Could not reach $apiBaseUrl';
|
|
debugPrint('BankController.refresh failed: $error\n$stackTrace');
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> _loadLinkedData() async {
|
|
try {
|
|
final results = await Future.wait([
|
|
_api.fetchBalance(),
|
|
_api.fetchTransactions(),
|
|
]);
|
|
_balance = results[0] as BankBalance;
|
|
_transactions = results[1] as List<BankTransaction>;
|
|
_error = null;
|
|
} on IngtrakApiException catch (error) {
|
|
if (error.isNotLinked) {
|
|
_status = ConnectStatus.disconnected;
|
|
_balance = null;
|
|
_transactions = const [];
|
|
_error = error.message;
|
|
} else {
|
|
_error = error.message;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `POST /api/connect`. Returns auth payload for the in-app WebView.
|
|
Future<ConnectStart?> beginConnect() async {
|
|
_isConnecting = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final start = await _api.startConnect();
|
|
if (start.authUrl.isEmpty) {
|
|
throw const IngtrakApiException('Connect response missing authUrl');
|
|
}
|
|
_pendingState = start.state;
|
|
return start;
|
|
} on IngtrakApiException catch (error, stackTrace) {
|
|
_error = error.message;
|
|
debugPrint('BankController.beginConnect failed: $error\n$stackTrace');
|
|
return null;
|
|
} catch (error, stackTrace) {
|
|
_error = 'Could not start bank connect.';
|
|
debugPrint('BankController.beginConnect failed: $error\n$stackTrace');
|
|
return null;
|
|
} finally {
|
|
_isConnecting = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Handle optional `ingtrak://connected?...` (e.g. after HTML bounce).
|
|
Future<bool> handleRedirect(Uri uri) async {
|
|
if (uri.scheme != kBankRedirectScheme) return false;
|
|
if (uri.host != kBankRedirectHost && uri.path != '/$kBankRedirectHost') {
|
|
if (uri.host.isNotEmpty && uri.host != kBankRedirectHost) return false;
|
|
}
|
|
|
|
final code = uri.queryParameters['code'];
|
|
if (code != null && code.isNotEmpty) {
|
|
final state = uri.queryParameters['state'] ?? _pendingState;
|
|
return completeWithCode(code: code, state: state);
|
|
}
|
|
|
|
// Backend already exchanged the code; just refresh local state.
|
|
if (uri.queryParameters['linked'] == '1') {
|
|
await refresh();
|
|
if (!isLinked) {
|
|
_error =
|
|
'Returned to the app, but the backend still has no linked session. '
|
|
'Finish login inside the in-app WebView (do not open the email '
|
|
'link in another browser), then pull to refresh.';
|
|
notifyListeners();
|
|
}
|
|
return isLinked;
|
|
}
|
|
|
|
final redirectError = uri.queryParameters['error'];
|
|
_error = redirectError != null
|
|
? 'Bank auth failed: $redirectError'
|
|
: 'Bank redirect missing code';
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
|
|
Future<bool> completeWithCode({
|
|
required String code,
|
|
String? state,
|
|
}) async {
|
|
_isLoading = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_status = await _api.completeConnect(
|
|
code: code,
|
|
state: state ?? _pendingState,
|
|
);
|
|
_pendingState = null;
|
|
if (_status.linked) {
|
|
await _loadLinkedData();
|
|
}
|
|
return _status.linked;
|
|
} on IngtrakApiException catch (error, stackTrace) {
|
|
_error = error.message;
|
|
debugPrint('BankController.completeWithCode failed: $error\n$stackTrace');
|
|
return false;
|
|
} catch (error, stackTrace) {
|
|
_error = 'Could not complete bank connect.';
|
|
debugPrint('BankController.completeWithCode failed: $error\n$stackTrace');
|
|
return false;
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_api.close();
|
|
super.dispose();
|
|
}
|
|
}
|