mirror of
https://github.com/we-promise/sure.git
synced 2026-08-06 00:52:16 +00:00
* feat(mobile): add privacy-safe Sentry instrumentation * fix(mobile): harden auth telemetry handling * fix(mobile): harden Sentry privacy filters * fix(mobile): make auth session persistence atomic
244 lines
8.2 KiB
Dart
244 lines
8.2 KiB
Dart
import 'dart:io';
|
|
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import '../models/account.dart';
|
|
import '../services/accounts_service.dart';
|
|
import '../services/balance_sheet_service.dart';
|
|
import '../services/offline_storage_service.dart';
|
|
import '../services/connectivity_service.dart';
|
|
import '../services/log_service.dart';
|
|
|
|
class AccountsProvider with ChangeNotifier {
|
|
final AccountsService _accountsService = AccountsService();
|
|
final BalanceSheetService _balanceSheetService = BalanceSheetService();
|
|
final OfflineStorageService _offlineStorage = OfflineStorageService();
|
|
final LogService _log = LogService.instance;
|
|
|
|
List<Account> _accounts = [];
|
|
bool _isLoading = false;
|
|
bool _isInitializing = true;
|
|
String? _errorMessage;
|
|
Map<String, dynamic>? _pagination;
|
|
ConnectivityService? _connectivityService;
|
|
|
|
// Summary / net worth data
|
|
String? _netWorthFormatted;
|
|
String? _assetsFormatted;
|
|
String? _liabilitiesFormatted;
|
|
String? _familyCurrency;
|
|
bool _isBalanceSheetStale = false;
|
|
|
|
List<Account> get accounts => _accounts;
|
|
bool get isLoading => _isLoading;
|
|
bool get isInitializing => _isInitializing;
|
|
String? get errorMessage => _errorMessage;
|
|
Map<String, dynamic>? get pagination => _pagination;
|
|
String? get netWorthFormatted => _netWorthFormatted;
|
|
String? get assetsFormatted => _assetsFormatted;
|
|
String? get liabilitiesFormatted => _liabilitiesFormatted;
|
|
String? get familyCurrency => _familyCurrency;
|
|
bool get isBalanceSheetStale => _isBalanceSheetStale;
|
|
|
|
List<Account> get assetAccounts {
|
|
final assets = _accounts.where((a) => a.isAsset).toList();
|
|
_sortAccounts(assets);
|
|
return assets;
|
|
}
|
|
|
|
List<Account> get liabilityAccounts {
|
|
final liabilities = _accounts.where((a) => a.isLiability).toList();
|
|
_sortAccounts(liabilities);
|
|
return liabilities;
|
|
}
|
|
|
|
Map<String, double> get assetTotalsByCurrency {
|
|
final totals = <String, double>{};
|
|
for (var account in _accounts.where((a) => a.isAsset)) {
|
|
totals[account.currency] =
|
|
(totals[account.currency] ?? 0.0) + account.balanceAsDouble;
|
|
}
|
|
return totals;
|
|
}
|
|
|
|
Map<String, double> get liabilityTotalsByCurrency {
|
|
final totals = <String, double>{};
|
|
for (var account in _accounts.where((a) => a.isLiability)) {
|
|
totals[account.currency] =
|
|
(totals[account.currency] ?? 0.0) + account.balanceAsDouble;
|
|
}
|
|
return totals;
|
|
}
|
|
|
|
void setConnectivityService(ConnectivityService service) {
|
|
_connectivityService = service;
|
|
}
|
|
|
|
void _sortAccounts(List<Account> accounts) {
|
|
accounts.sort((a, b) {
|
|
// 1. Sort by account type
|
|
int typeComparison = a.accountType.compareTo(b.accountType);
|
|
if (typeComparison != 0) return typeComparison;
|
|
|
|
// 2. Sort by currency
|
|
int currencyComparison = a.currency.compareTo(b.currency);
|
|
if (currencyComparison != 0) return currencyComparison;
|
|
|
|
// 3. Sort by balance (descending - highest first)
|
|
int balanceComparison = b.balanceAsDouble.compareTo(a.balanceAsDouble);
|
|
if (balanceComparison != 0) return balanceComparison;
|
|
|
|
// 4. Sort by name
|
|
return a.name.compareTo(b.name);
|
|
});
|
|
}
|
|
|
|
/// Fetch accounts (offline-first approach)
|
|
Future<bool> fetchAccounts({
|
|
required String accessToken,
|
|
int page = 1,
|
|
int perPage = 25,
|
|
bool forceSync = false,
|
|
}) async {
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
// Always load from local storage first for instant display
|
|
final cachedAccounts = await _offlineStorage.getAccounts();
|
|
if (cachedAccounts.isNotEmpty) {
|
|
_accounts = cachedAccounts;
|
|
_isInitializing = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
// If online and (force sync or no cached data), fetch from server
|
|
final isOnline = _connectivityService?.isOnline ?? false;
|
|
if (isOnline && (forceSync || cachedAccounts.isEmpty)) {
|
|
final result = await _accountsService.getAccounts(
|
|
accessToken: accessToken,
|
|
page: page,
|
|
perPage: perPage,
|
|
);
|
|
|
|
if (result['success'] == true && result.containsKey('accounts')) {
|
|
final serverAccounts =
|
|
(result['accounts'] as List<dynamic>?)?.cast<Account>() ?? [];
|
|
_pagination = result['pagination'] as Map<String, dynamic>?;
|
|
|
|
// Save to local cache
|
|
await _offlineStorage.clearAccounts();
|
|
await _offlineStorage.saveAccounts(serverAccounts);
|
|
|
|
// Update in-memory accounts
|
|
_accounts = serverAccounts;
|
|
_errorMessage = null;
|
|
} else {
|
|
// If server fetch failed but we have cached data, that's OK
|
|
if (_accounts.isEmpty) {
|
|
_errorMessage =
|
|
result['error'] as String? ?? 'Failed to fetch accounts';
|
|
}
|
|
}
|
|
} else if (!isOnline && _accounts.isEmpty) {
|
|
_errorMessage =
|
|
'You are offline. Please connect to the internet to load accounts.';
|
|
}
|
|
|
|
// Fetch balance sheet independently — works even with cached accounts
|
|
if (isOnline) {
|
|
await _fetchBalanceSheet(accessToken);
|
|
}
|
|
|
|
_isLoading = false;
|
|
_isInitializing = false;
|
|
notifyListeners();
|
|
return _accounts.isNotEmpty;
|
|
} catch (e) {
|
|
_log.error(
|
|
'AccountsProvider',
|
|
'fetchAccounts failed with ${e.runtimeType}',
|
|
);
|
|
// If we have cached accounts, show them even if sync fails
|
|
if (_accounts.isEmpty) {
|
|
// Provide more specific error messages based on exception type
|
|
if (e is SocketException) {
|
|
_errorMessage =
|
|
'Network error. Please check your internet connection and try again.';
|
|
} else if (e is TimeoutException) {
|
|
_errorMessage =
|
|
'Request timed out. Please check your connection and try again.';
|
|
} else if (e is FormatException) {
|
|
_errorMessage = 'Server response error. Please try again later.';
|
|
} else if (e.toString().contains('401') ||
|
|
e.toString().contains('unauthorized')) {
|
|
_errorMessage = 'unauthorized';
|
|
} else if (e.toString().contains('HandshakeException') ||
|
|
e.toString().contains('certificate') ||
|
|
e.toString().contains('SSL')) {
|
|
_errorMessage =
|
|
'Secure connection error. Please check your internet connection and try again.';
|
|
} else {
|
|
_errorMessage = 'Something went wrong. Please try again.';
|
|
}
|
|
}
|
|
_isLoading = false;
|
|
_isInitializing = false;
|
|
notifyListeners();
|
|
return _accounts.isNotEmpty;
|
|
}
|
|
}
|
|
|
|
/// Fetches balance sheet data and updates formatted net worth, assets,
|
|
/// and liabilities values for display. On failure, marks the existing
|
|
/// values as stale rather than clearing them.
|
|
Future<void> _fetchBalanceSheet(String accessToken) async {
|
|
try {
|
|
final result =
|
|
await _balanceSheetService.getBalanceSheet(accessToken: accessToken);
|
|
if (result['success'] == true) {
|
|
_familyCurrency = result['currency'] as String?;
|
|
final netWorth = result['net_worth'] as Map<String, dynamic>?;
|
|
final assets = result['assets'] as Map<String, dynamic>?;
|
|
final liabilities = result['liabilities'] as Map<String, dynamic>?;
|
|
_netWorthFormatted = netWorth?['formatted'] as String?;
|
|
_assetsFormatted = assets?['formatted'] as String?;
|
|
_liabilitiesFormatted = liabilities?['formatted'] as String?;
|
|
_isBalanceSheetStale = false;
|
|
} else {
|
|
// Keep existing values but mark as stale
|
|
if (_netWorthFormatted != null) {
|
|
_isBalanceSheetStale = true;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
_log.error(
|
|
'AccountsProvider',
|
|
'Balance sheet fetch failed with ${e.runtimeType}',
|
|
);
|
|
// Keep existing values but mark as stale
|
|
if (_netWorthFormatted != null) {
|
|
_isBalanceSheetStale = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void clearAccounts() {
|
|
_accounts = [];
|
|
_pagination = null;
|
|
_errorMessage = null;
|
|
_isInitializing = true;
|
|
_netWorthFormatted = null;
|
|
_assetsFormatted = null;
|
|
_liabilitiesFormatted = null;
|
|
_familyCurrency = null;
|
|
_isBalanceSheetStale = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
void clearError() {
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
}
|
|
}
|