Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import '../../../../../pages_desktop_specific/desktop_home_view.dart';
import '../../../../../providers/frost_wallet/frost_wallet_providers.dart';
import '../../../../../providers/global/secure_store_provider.dart';
import '../../../../../providers/providers.dart';
import '../../../../../services/transaction_notification_tracker.dart';
import '../../../../../themes/stack_colors.dart';
import '../../../../../utilities/assets.dart';
import '../../../../../utilities/logger.dart';
Expand Down Expand Up @@ -141,6 +142,9 @@ class _FrostCreateStep5State extends ConsumerState<FrostCreateStep5> {
coin: data.info.frostCurrency,
name: data.info.walletName,
);
await TransactionNotificationTracker(
walletId: info.walletId,
).markInitialized();

final wallet = await Wallet.create(
walletInfo: info,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ class _NewWalletRecoveryPhraseWarningViewState
}

final txTracker = TransactionNotificationTracker(walletId: info.walletId);
await txTracker.markInitialized();

String? mnemonicPassphrase;
String? mnemonic;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import '../../../pages_desktop_specific/desktop_home_view.dart';
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import '../../../providers/global/secure_store_provider.dart';
import '../../../providers/providers.dart';
import '../../../services/transaction_notification_tracker.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/address_utils.dart';
import '../../../utilities/assets.dart';
Expand Down Expand Up @@ -327,10 +326,6 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
.save(node, null, false);
}

final txTracker = TransactionNotificationTracker(
walletId: info.walletId,
);

try {
final wallet = await Wallet.create(
walletInfo: info,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'dart:async';

import 'package:event_bus/event_bus.dart';

import '../../../../wallets/crypto_currency/crypto_currency.dart';

abstract class CryptoNotificationsEventBus {
static final instance = EventBus();
static int _listenerCount = 0;

static bool get hasListeners => _listenerCount > 0;

static void registerListener() => _listenerCount++;

static void unregisterListener() {
assert(_listenerCount > 0);
if (_listenerCount > 0) {
_listenerCount--;
}
}
}

class CryptoNotificationEvent {
final String title;
final String walletId;
final String walletName;
final DateTime date;
final bool shouldWatchForUpdates;
final CryptoCurrency coin;
final String? txid;
final int? confirmations;
final int? requiredConfirmations;
final String? changeNowId;
final String? payload;
final Completer<void> _deliveryCompleter = Completer<void>();

Future<void> get delivered => _deliveryCompleter.future;

CryptoNotificationEvent({
required this.title,
required this.walletId,
required this.walletName,
required this.date,
required this.shouldWatchForUpdates,
required this.coin,
this.txid,
this.confirmations,
this.requiredConfirmations,
this.changeNowId,
this.payload,
});

void completeDelivery() {
if (!_deliveryCompleter.isCompleted) {
_deliveryCompleter.complete();
}
}

void failDelivery(Object error, StackTrace stackTrace) {
if (!_deliveryCompleter.isCompleted) {
_deliveryCompleter.completeError(error, stackTrace);
}
}
}
68 changes: 53 additions & 15 deletions lib/services/notifications_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*
*/

import 'dart:async';
import 'dart:io';

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

Expand All @@ -18,7 +18,8 @@ import '../utilities/prefs.dart';
import 'notifications_service.dart';

abstract final class NotificationApi {
static Completer<void>? _initCalledCompleter;
static Future<void>? _initializationFuture;
static Future<bool?>? _permissionRequestFuture;
static final _notifications = FlutterLocalNotificationsPlugin();
// static final onNotifications = BehaviorSubject<String?>();

Expand All @@ -38,16 +39,24 @@ abstract final class NotificationApi {
}

static Future<void> init({bool initScheduled = false}) async {
if (_initCalledCompleter == null) {
_initCalledCompleter = Completer<void>();
} else {
if (_initCalledCompleter!.isCompleted) {
return;
} else {
return await _initCalledCompleter!.future;
final existing = _initializationFuture;
if (existing != null) {
return existing;
}

final initialization = _initialize();
_initializationFuture = initialization;
try {
await initialization;
} catch (_) {
if (identical(_initializationFuture, initialization)) {
_initializationFuture = null;
}
rethrow;
}
}

static Future<void> _initialize() async {
const android = AndroidInitializationSettings('app_icon_alpha');
const iOS = DarwinInitializationSettings();
const linux = LinuxInitializationSettings(
Expand All @@ -69,7 +78,21 @@ abstract final class NotificationApi {
// onNotifications.add(payload.payload);
// },
);
_initCalledCompleter!.complete();
}

static Future<void> _requestPermissionIfNeeded() async {
if (!Platform.isAndroid) {
return;
}
_permissionRequestFuture ??= _notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.requestNotificationsPermission();
final granted = await _permissionRequestFuture;
if (granted == false) {
Logging.instance.i("System notification permission was denied");
}
}

static Future<void> clearNotifications() async {
Expand All @@ -92,6 +115,7 @@ abstract final class NotificationApi {
String? payload,
}) async {
await init();
await _requestPermissionIfNeeded();
final id = await prefs.incrementCurrentNotificationIndex();
await _notifications.show(
id,
Expand All @@ -117,11 +141,9 @@ abstract final class NotificationApi {
String? changeNowId,
String? payload,
}) async {
final id = await _showOsNotification(
title: title,
body: body,
payload: payload,
);
await init();
await _requestPermissionIfNeeded();
final id = await prefs.incrementCurrentNotificationIndex();

String confirms = "";
if (txid != null &&
Expand All @@ -145,6 +167,22 @@ abstract final class NotificationApi {
);

await notificationsService.add(model, true);

try {
await _notifications.show(
id,
title,
body,
await _notificationDetails(),
payload: payload,
);
} catch (error, stackTrace) {
Logging.instance.w(
"System notification delivery failed",
error: error,
stackTrace: stackTrace,
);
}
}

static Future<void> showLocalOnly({
Expand Down
4 changes: 2 additions & 2 deletions lib/services/notifications_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,10 @@ class NotificationsService extends ChangeNotifier {
);
if (notification.shouldWatchForUpdates) {
if (notification.txid != null) {
_addWatchedTxNotification(notification);
await _addWatchedTxNotification(notification);
}
if (notification.changeNowId != null) {
_addWatchedTradeNotification(notification);
await _addWatchedTradeNotification(notification);
}
}
if (shouldNotifyListeners) {
Expand Down
82 changes: 82 additions & 0 deletions lib/services/transaction_notification_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import '../models/isar/models/blockchain_data/transaction.dart';
import '../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import 'event_bus/events/global/crypto_notification_event.dart';
import 'transaction_notification_tracker.dart';

typedef CryptoNotificationSink =
Future<void> Function(CryptoNotificationEvent event);

class TransactionNotificationService {
TransactionNotificationService({
required this.store,
CryptoNotificationSink? notificationSink,
}) : _notificationSink = notificationSink ?? _deliverThroughEventBus;

final TransactionNotificationStore store;
final CryptoNotificationSink _notificationSink;

static Future<void> _deliverThroughEventBus(
CryptoNotificationEvent event,
) async {
if (!CryptoNotificationsEventBus.hasListeners) {
throw StateError("No crypto notification listener is registered");
}
CryptoNotificationsEventBus.instance.fire(event);
await event.delivered.timeout(const Duration(seconds: 30));
}

Future<void> notifyNewIncomingTransactions({
required Set<String> knownTxids,
required Iterable<TransactionV2> transactions,
required CryptoCurrency coin,
required String walletId,
required String walletName,
required int chainHeight,
required bool supportsConfirmationUpdates,
}) async {
final deliveredTxids = store.deliveredTxids;
final incoming = transactions
.where((tx) => tx.type == TransactionType.incoming)
.where((tx) => !knownTxids.contains(tx.txid))
.where((tx) => !deliveredTxids.contains(tx.txid))
.toList();

if (!store.isInitialized && knownTxids.isEmpty) {
await store.markInitialized();
return;
}

if (!store.isInitialized) {
await store.markInitialized();
}

for (final tx in incoming) {
final amount = tx.getAmountReceivedInThisWallet(
fractionDigits: coin.fractionDigits,
);
final formattedAmount = amount.decimal.toStringAsFixed(
coin.fractionDigits,
);
final payload = "$formattedAmount ${coin.ticker}";

final event = CryptoNotificationEvent(
title: "Incoming ${coin.prettyName} transaction",
walletId: walletId,
walletName: walletName,
date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
shouldWatchForUpdates:
supportsConfirmationUpdates &&
(tx.height == null || tx.height! <= 0),
coin: coin,
txid: tx.txid,
confirmations: tx.getConfirmations(chainHeight),
requiredConfirmations: coin.minConfirms,
payload: payload,
);

await _notificationSink(event);
await store.recordDelivered(tx.txid);
}
}
}
Loading
Loading