diff --git a/.env.example b/.env.example index ba4ce97..3fbca7a 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -EXCHANGE_RATE_API_KEY=REPLACE_WITH_YOUR_EXCHANGE_RATE_API_KEY \ No newline at end of file +FOREX_RATE_API_KEY=REPLACE_WITH_YOUR_FOREXRATE_API_KEY \ No newline at end of file diff --git a/README.md b/README.md index 7b9dab7..c7acd0e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Simple Flutter app to track US rental income and convert to SEK for Swedish tax 1. Create `.env` in the project root with your FX API key: ```env -EXCHANGE_RATE_API_KEY=REPLACE_WITH_YOUR_EXCHANGE_RATE_API_KEY +FOREX_RATE_API_KEY=REPLACE_WITH_YOUR_FOREXRATE_API_KEY ``` 1. Install dependencies: diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 25de8b9..80b266d 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -198,7 +198,7 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 16), const Text( - 'ExchangeratesAPI key is loaded from .env (EXCHANGE_RATE_API_KEY).', + 'ForexRateAPI key is loaded from .env (FOREX_RATE_API_KEY).', ), ], ), diff --git a/lib/services/open_exchange_service.dart b/lib/services/open_exchange_service.dart index 4088cc7..c28a8f2 100644 --- a/lib/services/open_exchange_service.dart +++ b/lib/services/open_exchange_service.dart @@ -26,8 +26,8 @@ class ForexRateApiService { String _redactedUri(Uri uri) { final redactedQuery = {...uri.queryParameters}; - if (redactedQuery.containsKey('access_key')) { - redactedQuery['access_key'] = '***REDACTED***'; + if (redactedQuery.containsKey('api_key')) { + redactedQuery['api_key'] = '***REDACTED***'; } return uri.replace(queryParameters: redactedQuery).toString(); } @@ -74,26 +74,22 @@ class ForexRateApiService { } Future fetchUsdToSekRate({required DateTime estDate}) async { - if (apiKey == 'REPLACE_WITH_YOUR_EXCHANGE_RATE_API_KEY') { - throw StateError('ExchangeratesAPI key is not configured.'); + if (apiKey == 'REPLACE_WITH_YOUR_FOREXRATE_API_KEY') { + throw StateError('ForexRateAPI key is not configured.'); } final date = DateFormat('yyyy-MM-dd').format(estDate); - final uri = Uri.https( - 'api.exchangeratesapi.io', - '/v1/$date', - // Free-tier plans can restrict custom base currencies. - // We request EUR-based USD and SEK rates and derive USD->SEK via cross-rate. - {'access_key': apiKey, 'symbols': 'USD,SEK'}, - ); + final uri = Uri.https('api.forexrateapi.com', '/v1/$date', { + 'api_key': apiKey, + 'base': 'USD', + 'currencies': 'SEK', + }); _log('Daily rate request: ${_redactedUri(uri)}'); final response = await _getWithRateLimitRetry(uri, operation: 'Daily rate'); if (response.statusCode != 200) { - throw StateError( - 'ExchangeratesAPI request failed (${response.statusCode}).', - ); + throw StateError('ForexRateAPI request failed (${response.statusCode}).'); } final json = jsonDecode(response.body) as Map; @@ -101,37 +97,26 @@ class ForexRateApiService { if (!success) { final error = json['error']; _log('Daily rate success=false. error=$error'); - throw StateError('ExchangeratesAPI returned success=false. error=$error'); + throw StateError('ForexRateAPI returned success=false. error=$error'); } final rates = json['rates'] as Map?; - final usdPerEur = rates?['USD']; - final sekPerEur = rates?['SEK']; - if (usdPerEur == null || sekPerEur == null) { - throw StateError('USD/SEK rates were missing in API response.'); + final sek = rates?['SEK']; + if (sek == null) { + throw StateError('SEK rate was missing in API response.'); } - final usdPerEurValue = (usdPerEur as num).toDouble(); - final sekPerEurValue = (sekPerEur as num).toDouble(); - if (usdPerEurValue == 0) { - throw StateError('USD rate was zero; cannot derive USD->SEK.'); - } + _log('Daily rate parsed SEK quote: $sek'); - final usdToSek = sekPerEurValue / usdPerEurValue; - - _log( - 'Daily rate parsed usdPerEur=$usdPerEurValue sekPerEur=$sekPerEurValue derivedUsdToSek=$usdToSek', - ); - - return usdToSek; + return (sek as num).toDouble(); } Future convertUsdToSek({ required DateTime estDate, required double amount, }) async { - if (apiKey == 'REPLACE_WITH_YOUR_EXCHANGE_RATE_API_KEY') { - throw StateError('ExchangeratesAPI key is not configured.'); + if (apiKey == 'REPLACE_WITH_YOUR_FOREXRATE_API_KEY') { + throw StateError('ForexRateAPI key is not configured.'); } final quote = await fetchUsdToSekRate(estDate: estDate); @@ -148,70 +133,9 @@ class ForexRateApiService { /// API call. Returns a map of 'yyyy-MM-dd' -> rate, or null if the timeseries /// endpoint is not available on the current subscription plan. Future?> tryFetchYearRates(int year) async { - if (apiKey == 'REPLACE_WITH_YOUR_EXCHANGE_RATE_API_KEY') { - throw StateError('ExchangeratesAPI key is not configured.'); + if (apiKey == 'REPLACE_WITH_YOUR_FOREXRATE_API_KEY') { + throw StateError('ForexRateAPI key is not configured.'); } - - final uri = - Uri.https('api.exchangeratesapi.io', '/v1/timeseries', { - 'access_key': apiKey, - 'start_date': '$year-01-01', - 'end_date': '$year-12-31', - 'symbols': 'USD,SEK', - }); - - _log('Timeseries request: ${_redactedUri(uri)}'); - - final response = await _getWithRateLimitRetry(uri, operation: 'Timeseries'); - - if (response.statusCode == 403) { - _log( - 'Timeseries endpoint not available on current plan; falling back to daily requests.', - ); - return null; - } - - if (response.statusCode != 200) { - throw StateError( - 'ExchangeratesAPI timeseries failed (${response.statusCode}).', - ); - } - - final json = jsonDecode(response.body) as Map; - final success = json['success'] as bool? ?? false; - if (!success) { - final error = json['error'] as Map?; - final code = error?['code']?.toString() ?? ''; - if (code == 'function_access_restricted' || - code == 'subscription_plan_not_support_endpoint') { - _log( - 'Timeseries endpoint not available on current plan; falling back to daily requests.', - ); - return null; - } - _log('Timeseries success=false. error=$error'); - throw StateError( - 'ExchangeratesAPI timeseries returned success=false. error=$error', - ); - } - - final rates = json['rates'] as Map?; - if (rates == null) { - throw StateError('Timeseries response missing rates object.'); - } - - final result = {}; - for (final entry in rates.entries) { - final date = entry.key; - final dayRates = entry.value as Map?; - final usdPerEur = (dayRates?['USD'] as num?)?.toDouble(); - final sekPerEur = (dayRates?['SEK'] as num?)?.toDouble(); - if (usdPerEur != null && sekPerEur != null && usdPerEur != 0) { - result[date] = sekPerEur / usdPerEur; - } - } - - _log('Timeseries parsed ${result.length} rate entries for year $year'); - return result; + return null; } } diff --git a/lib/state/rent_controller.dart b/lib/state/rent_controller.dart index dfc4c65..38d21d0 100644 --- a/lib/state/rent_controller.dart +++ b/lib/state/rent_controller.dart @@ -56,14 +56,14 @@ class RentController extends ChangeNotifier { } static String _exchangeRateApiKeyFromEnv() { - final key = dotenv.env['EXCHANGE_RATE_API_KEY']?.trim() ?? ''; + final key = dotenv.env['FOREX_RATE_API_KEY']?.trim() ?? ''; if (key.isEmpty) { if (kDebugMode) { debugPrint( - '[RentController] EXCHANGE_RATE_API_KEY missing in .env, API calls will fail until set.', + '[RentController] FOREX_RATE_API_KEY missing in .env, API calls will fail until set.', ); } - return 'REPLACE_WITH_YOUR_EXCHANGE_RATE_API_KEY'; + return 'REPLACE_WITH_YOUR_FOREXRATE_API_KEY'; } return key; }