Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .fvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"flutter": "3.35.7"
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,6 @@ chrome/.packages
.pdm
bfg-1.14.0.jar
lib/generated/

# FVM Version Cache
.fvm/
4 changes: 4 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_bg_service_small" />
<!-- Preserve ic_notification for local notifications -->
<meta-data
android:name="com.app.notification_icon"
android:resource="@drawable/ic_notification" />

</application>

Expand Down
4 changes: 3 additions & 1 deletion lib/background/background.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ Future<void> serviceMain(ServiceInstance service) async {

subscription.listen((event) async {
try {
if (await eventStore.hasItem(event.id!)) return;
if (await eventStore.hasItem(event.id!)) {
return;
}
await notification_service.retryNotification(event);
} catch (e) {
Logger().e('Error processing event', error: e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,18 @@ Future<void> showLocalNotification(NostrEvent event) async {
try {
final mostroMessage = await _decryptAndProcessEvent(event);
if (mostroMessage == null) return;


final sessions = await _loadSessionsFromDatabase();
final matchingSession = sessions.cast<Session?>().firstWhere(
(session) => session?.orderId == mostroMessage.id,
orElse: () => null,
);

final notificationData = await NotificationDataExtractor.extractFromMostroMessage(mostroMessage, null, session: matchingSession);
if (notificationData == null || notificationData.isTemporary) return;

if (notificationData == null || notificationData.isTemporary) {
return;
}

final notificationText = await _getLocalizedNotificationText(notificationData.action, notificationData.values);
final expandedText = _getExpandedText(notificationData.values);
Expand All @@ -79,7 +81,7 @@ Future<void> showLocalNotification(NostrEvent event) async {
enableVibration: true,
ticker: notificationText.title,
icon: '@drawable/ic_notification',
styleInformation: expandedText != null
styleInformation: expandedText != null
? BigTextStyleInformation(expandedText, contentTitle: notificationText.title)
: null,
category: AndroidNotificationCategory.message,
Expand Down Expand Up @@ -111,25 +113,34 @@ Future<void> showLocalNotification(NostrEvent event) async {

Future<MostroMessage?> _decryptAndProcessEvent(NostrEvent event) async {
try {
if (event.kind != 4 && event.kind != 1059) return null;
if (event.kind != 4 && event.kind != 1059) {
return null;
}

final sessions = await _loadSessionsFromDatabase();

final matchingSession = sessions.cast<Session?>().firstWhere(
(s) => s?.tradeKey.public == event.recipient,
orElse: () => null,
);

if (matchingSession == null) return null;
if (matchingSession == null) {
return null;
}

final decryptedEvent = await event.unWrap(matchingSession.tradeKey.private);
if (decryptedEvent.content == null) return null;
if (decryptedEvent.content == null) {
return null;
}

final result = jsonDecode(decryptedEvent.content!);
if (result is! List || result.isEmpty) return null;
if (result is! List || result.isEmpty) {
return null;
}

final mostroMessage = MostroMessage.fromJson(result[0]);
mostroMessage.timestamp = event.createdAt?.millisecondsSinceEpoch;

return mostroMessage;
} catch (e) {
Logger().e('Decrypt error: $e');
Expand Down Expand Up @@ -258,25 +269,25 @@ String? _getExpandedText(Map<String, dynamic> values) {
}


Future<void> retryNotification(NostrEvent event, {int maxAttempts = 3}) async {
int attempt = 0;
bool success = false;
while (!success && attempt < maxAttempts) {
try {
await showLocalNotification(event);
success = true;
} catch (e) {
attempt++;
if (attempt >= maxAttempts) {
Logger().e('Failed to show notification after $maxAttempts attempts: $e');
break;
}
// Exponential backoff: 1s, 2s, 4s, etc.
final backoffSeconds = pow(2, attempt - 1).toInt();
Logger().e('Notification attempt $attempt failed: $e. Retrying in ${backoffSeconds}s');
await Future.delayed(Duration(seconds: backoffSeconds));
}
}
Future<void> retryNotification(NostrEvent event, {int maxAttempts = 3}) async {
int attempt = 0;
bool success = false;

while (!success && attempt < maxAttempts) {
try {
await showLocalNotification(event);
success = true;
} catch (e) {
attempt++;
if (attempt >= maxAttempts) {
Logger().e('Failed to show notification after $maxAttempts attempts: $e');
break;
}

// Exponential backoff: 1s, 2s, 4s, etc.
final backoffSeconds = pow(2, attempt - 1).toInt();
Logger().e('Notification attempt $attempt failed: $e. Retrying in ${backoffSeconds}s');
await Future.delayed(Duration(seconds: backoffSeconds));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,22 @@ class NotificationDataExtractor {
}
break;

case Action.fiatSent:
// Notification for seller when buyer marks fiat as sent
// This requires seller to release the funds
final order = event.getPayload<Order>();
if (order != null) {
values = {
'fiat_code': order.fiatCode,
'fiat_amount': order.fiatAmount,
};
}
break;

case Action.fiatSentOk:
// Only sellers should receive fiat confirmed notifications
if (session?.role != Role.seller) return null;

final peer = event.getPayload<Peer>();
if (peer?.publicKey != null) {
final buyerNym = ref != null
Expand Down