Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions lib/application/app_dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'package:subctrl/application/subscriptions/add_subscription_use_case.dart
import 'package:subctrl/application/subscriptions/delete_subscription_use_case.dart';
import 'package:subctrl/application/subscriptions/update_subscription_use_case.dart';
import 'package:subctrl/application/subscriptions/watch_subscriptions_use_case.dart';
import 'package:subctrl/application/support/fetch_donation_wallets_use_case.dart';
import 'package:subctrl/application/tags/create_tag_use_case.dart';
import 'package:subctrl/application/tags/delete_tag_use_case.dart';
import 'package:subctrl/application/tags/update_tag_use_case.dart';
Expand All @@ -48,6 +49,9 @@ import 'package:subctrl/infrastructure/repositories/drift_currency_repository.da
import 'package:subctrl/infrastructure/repositories/drift_settings_repository.dart';
import 'package:subctrl/infrastructure/repositories/drift_subscription_repository.dart';
import 'package:subctrl/infrastructure/repositories/drift_tag_repository.dart';
import 'package:subctrl/infrastructure/support/donation_wallet_fallback_data.dart';
import 'package:subctrl/infrastructure/support/remote_donation_wallets_provider.dart';
import 'package:subctrl/infrastructure/support/resilient_donation_wallets_provider.dart';

class AppDependencies {
AppDependencies._({
Expand Down Expand Up @@ -86,8 +90,11 @@ class AppDependencies {
required this.scheduleNotificationsUseCase,
required this.requestNotificationPermissionUseCase,
required this.openNotificationSettingsUseCase,
required this.fetchDonationWalletsUseCase,
required YahooFinanceCurrencyClient yahooFinanceCurrencyClient,
}) : _yahooFinanceCurrencyClient = yahooFinanceCurrencyClient;
required RemoteDonationWalletsProvider remoteDonationWalletsProvider,
}) : _yahooFinanceCurrencyClient = yahooFinanceCurrencyClient,
_remoteDonationWalletsProvider = remoteDonationWalletsProvider;

factory AppDependencies() {
final database = AppDatabase();
Expand All @@ -110,6 +117,13 @@ class AppDependencies {
yahooFinanceCurrencyClient: yahooFinanceClient,
currencyRepository: currencyRepository,
);
final remoteDonationWalletsProvider = RemoteDonationWalletsProvider(
endpoint: Uri.parse('https://gonfff.com/subctrl/assets/wallets.json'),
);
final donationWalletsProvider = ResilientDonationWalletsProvider(
primary: remoteDonationWalletsProvider,
fallbackWallets: donationWalletFallbackData,
);
final localNotificationsService = LocalNotificationsService();
final notificationPermissionService = NotificationPermissionService();

Expand Down Expand Up @@ -177,19 +191,22 @@ class AppDependencies {
getPendingNotificationsUseCase: GetPendingNotificationsUseCase(
localNotificationsService,
),
cancelNotificationsUseCase:
CancelNotificationsUseCase(localNotificationsService),
cancelNotificationsUseCase: CancelNotificationsUseCase(
localNotificationsService,
),
scheduleNotificationsUseCase: ScheduleNotificationsUseCase(
localNotificationsService,
),
requestNotificationPermissionUseCase:
RequestNotificationPermissionUseCase(
notificationPermissionService,
),
RequestNotificationPermissionUseCase(notificationPermissionService),
openNotificationSettingsUseCase: OpenNotificationSettingsUseCase(
notificationPermissionService,
),
fetchDonationWalletsUseCase: FetchDonationWalletsUseCase(
donationWalletsProvider,
),
yahooFinanceCurrencyClient: yahooFinanceClient,
remoteDonationWalletsProvider: remoteDonationWalletsProvider,
);
}

Expand Down Expand Up @@ -235,10 +252,13 @@ class AppDependencies {
final RequestNotificationPermissionUseCase
requestNotificationPermissionUseCase;
final OpenNotificationSettingsUseCase openNotificationSettingsUseCase;
final FetchDonationWalletsUseCase fetchDonationWalletsUseCase;

final YahooFinanceCurrencyClient _yahooFinanceCurrencyClient;
final RemoteDonationWalletsProvider _remoteDonationWalletsProvider;

void dispose() {
_yahooFinanceCurrencyClient.close();
_remoteDonationWalletsProvider.close();
}
}
12 changes: 12 additions & 0 deletions lib/application/support/fetch_donation_wallets_use_case.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:subctrl/domain/entities/donation_wallet.dart';
import 'package:subctrl/domain/services/donation_wallets_provider.dart';

class FetchDonationWalletsUseCase {
FetchDonationWalletsUseCase(this._provider);

final DonationWalletsProvider _provider;

Future<List<DonationWallet>> call() {
return _provider.fetchWallets();
}
}
15 changes: 15 additions & 0 deletions lib/domain/entities/donation_wallet.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class DonationWallet {
const DonationWallet({
required this.label,
required this.address,
this.currency,
this.name,
this.network,
});

final String label;
final String address;
final String? currency;
final String? name;
final String? network;
}
14 changes: 14 additions & 0 deletions lib/domain/services/donation_wallets_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:subctrl/domain/entities/donation_wallet.dart';

abstract class DonationWalletsProvider {
Future<List<DonationWallet>> fetchWallets();
}

class DonationWalletsFetchException implements Exception {
DonationWalletsFetchException(this.message);

final String message;

@override
String toString() => 'DonationWalletsFetchException: $message';
}
39 changes: 39 additions & 0 deletions lib/infrastructure/support/donation_wallet_fallback_data.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:subctrl/domain/entities/donation_wallet.dart';

const List<DonationWallet> donationWalletFallbackData = [
DonationWallet(
label: 'BTC',
address: 'bc1qjtjzlxel5mn3pvtps2u2wnfease44783r5nmhl',
currency: 'BTC',
name: 'Bitcoin',
network: 'Bitcoin',
),
DonationWallet(
label: 'USDT (TRON)',
address: 'TRsN3XgSXdeQz3yoGusW3f94KHsBNE62yR',
currency: 'USDT',
name: 'Tether',
network: 'TRON (TRC20)',
),
DonationWallet(
label: 'ETH',
address: '0xDf3275d97DF7Ba76d12ec0F82378C1e0628A5F6F',
currency: 'ETH',
name: 'Ethereum',
network: 'Ethereum',
),
DonationWallet(
label: 'TON',
address: 'UQCYgQSiRx5pk5E0ALzhz6WsFjuK3SyPiAe7vYG5uhidsyqj',
currency: 'TON',
name: 'Toncoin',
network: 'TON',
),
DonationWallet(
label: 'SOL',
address: '2KRt8ASpGasvMSaWZfgFfrFgb1LaUHzudHfGiEcF9vVK',
currency: 'SOL',
name: 'Solana',
network: 'Solana',
),
];
75 changes: 75 additions & 0 deletions lib/infrastructure/support/remote_donation_wallets_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'dart:convert';
import 'dart:developer' as developer;

import 'package:http/http.dart' as http;
import 'package:subctrl/domain/entities/donation_wallet.dart';
import 'package:subctrl/domain/services/donation_wallets_provider.dart';

class RemoteDonationWalletsProvider implements DonationWalletsProvider {
RemoteDonationWalletsProvider({
required Uri endpoint,
http.Client? httpClient,
}) : _endpoint = endpoint,
_httpClient = httpClient ?? http.Client();

final Uri _endpoint;
final http.Client _httpClient;

static const _logName = 'RemoteDonationWalletsProvider';

@override
Future<List<DonationWallet>> fetchWallets() async {
try {
final response = await _httpClient.get(_endpoint);
if (response.statusCode != 200) {
throw DonationWalletsFetchException(
'Wallet request failed with status ${response.statusCode}',
);
}
return _parseResponse(response.body);
} catch (error, stackTrace) {
developer.log(
'Failed to fetch donation wallets',
name: _logName,
error: error,
stackTrace: stackTrace,
);
if (error is DonationWalletsFetchException) {
rethrow;
}
throw DonationWalletsFetchException('Unable to load donation wallets.');
}
}

List<DonationWallet> _parseResponse(String body) {
final decoded = json.decode(body);
if (decoded is! Map<String, dynamic>) {
throw DonationWalletsFetchException('Malformed wallets payload.');
}
final wallets = decoded['wallets'];
if (wallets is! List) {
return const [];
}
final parsed = <DonationWallet>[];
for (final entry in wallets) {
if (entry is! Map<String, dynamic>) continue;
final label = entry['label'];
final address = entry['address'];
if (label is! String || address is! String) {
continue;
}
parsed.add(
DonationWallet(
label: label.trim(),
address: address.trim(),
currency: (entry['currency'] as String?)?.trim(),
name: (entry['name'] as String?)?.trim(),
network: (entry['network'] as String?)?.trim(),
),
);
}
return parsed;
}

void close() => _httpClient.close();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'dart:developer' as developer;

import 'package:subctrl/domain/entities/donation_wallet.dart';
import 'package:subctrl/domain/services/donation_wallets_provider.dart';

class ResilientDonationWalletsProvider implements DonationWalletsProvider {
ResilientDonationWalletsProvider({
required DonationWalletsProvider primary,
required List<DonationWallet> fallbackWallets,
}) : _primary = primary,
_fallbackWallets = List<DonationWallet>.unmodifiable(fallbackWallets);

final DonationWalletsProvider _primary;
final List<DonationWallet> _fallbackWallets;

static const _logName = 'ResilientDonationWalletsProvider';

@override
Future<List<DonationWallet>> fetchWallets() async {
try {
final wallets = await _primary.fetchWallets();
if (wallets.isNotEmpty) {
return wallets;
}
developer.log(
'Primary provider returned empty list, using fallback.',
name: _logName,
);
} catch (error, stackTrace) {
developer.log(
'Primary provider failed, returning fallback wallets.',
name: _logName,
error: error,
stackTrace: stackTrace,
);
}
return _fallbackWallets;
}
}
14 changes: 14 additions & 0 deletions lib/presentation/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ class AppLocalizations {
'settingsAboutSupport': 'Support the project',
'settingsAboutVersion': 'App version',
'settingsCopyAction': 'Copy',
'settingsSupportLoading': 'Loading wallets...',
'settingsSupportError':
'Failed to load wallets. Check your connection and try again.',
'settingsSupportRetry': 'Retry',
'settingsSupportEmpty': 'Wallet list is empty right now.',
'settingsCopySuccess': 'Copied to clipboard',
'settingsVersionUnknown': 'Unknown',
'settingsCurrenciesEnabledLabel': '{count} enabled',
Expand Down Expand Up @@ -228,6 +233,11 @@ class AppLocalizations {
'settingsAboutSupport': 'Поддержать проект',
'settingsAboutVersion': 'Версия приложения',
'settingsCopyAction': 'Скопировать',
'settingsSupportLoading': 'Загружаем кошельки...',
'settingsSupportError':
'Не удалось загрузить кошельки. Проверьте соединение и попробуйте ещё раз.',
'settingsSupportRetry': 'Повторить',
'settingsSupportEmpty': 'Список кошельков пока пуст.',
'settingsCopySuccess': 'Скопировано',
'settingsVersionUnknown': 'Неизвестно',
'settingsCurrenciesEnabledLabel': '{count} активны',
Expand Down Expand Up @@ -422,6 +432,10 @@ class AppLocalizations {
String get settingsAboutSupport => _strings['settingsAboutSupport']!;
String get settingsAboutVersion => _strings['settingsAboutVersion']!;
String get settingsCopyAction => _strings['settingsCopyAction']!;
String get settingsSupportLoading => _strings['settingsSupportLoading']!;
String get settingsSupportError => _strings['settingsSupportError']!;
String get settingsSupportRetry => _strings['settingsSupportRetry']!;
String get settingsSupportEmpty => _strings['settingsSupportEmpty']!;
String get settingsCopySuccess => _strings['settingsCopySuccess']!;
String get settingsVersionUnknown => _strings['settingsVersionUnknown']!;
String get settingsCurrenciesTitle => _strings['settingsCurrenciesTitle']!;
Expand Down
6 changes: 5 additions & 1 deletion lib/presentation/screens/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _openSupport() async {
await Navigator.of(context).push(
CupertinoPageRoute<void>(
builder: (context) => SupportScreen(onClose: widget.onRequestClose),
builder: (context) => SupportScreen(
onClose: widget.onRequestClose,
fetchDonationWalletsUseCase:
widget.dependencies.fetchDonationWalletsUseCase,
),
),
);
}
Expand Down
Loading
Loading