Skip to content

Commit 1739fdd

Browse files
authored
Merge pull request #241 from MostroP2P/payment-failed
feat: add basic support for PaymentFailure and NextTrade payload types
2 parents 733ea90 + b5f40df commit 1739fdd

File tree

14 files changed

+212
-9
lines changed

14 files changed

+212
-9
lines changed

lib/core/mostro_fsm.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,29 @@ class MostroFSM {
102102
Status.waitingPayment: {
103103
Role.seller: {
104104
Action.payInvoice: Status.active,
105+
Action.paymentFailed: Status.paymentFailed,
105106
Action.cancel: Status.canceled,
106107
},
107108
Role.buyer: {
109+
Action.paymentFailed: Status.paymentFailed,
108110
Action.cancel: Status.canceled,
109111
},
110112
Role.admin: {},
111113
},
114+
// ───────────────────────── PAYMENT FAILED ────────────────────
115+
Status.paymentFailed: {
116+
Role.buyer: {
117+
Action.addInvoice: Status.waitingPayment,
118+
Action.cancel: Status.canceled,
119+
Action.dispute: Status.dispute,
120+
},
121+
Role.seller: {
122+
Action.payInvoice: Status.active,
123+
Action.cancel: Status.canceled,
124+
Action.dispute: Status.dispute,
125+
},
126+
Role.admin: {},
127+
},
112128
// ───────────────────────── ACTIVE ────────────────────────────
113129
Status.active: {
114130
Role.buyer: {

lib/data/models.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export 'package:mostro_mobile/data/models/peer.dart';
1010
export 'package:mostro_mobile/data/models/rating_user.dart';
1111
export 'package:mostro_mobile/data/models/rating.dart';
1212
export 'package:mostro_mobile/data/models/session.dart';
13+
export 'package:mostro_mobile/data/models/payment_failed.dart';
14+
export 'package:mostro_mobile/data/models/next_trade.dart';

lib/data/models/enums/status.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ enum Status {
1212
success('success'),
1313
waitingBuyerInvoice('waiting-buyer-invoice'),
1414
waitingPayment('waiting-payment'),
15+
paymentFailed('payment-failed'),
1516
cooperativelyCanceled('cooperatively-canceled'),
1617
inProgress('in-progress');
1718

lib/data/models/next_trade.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'package:mostro_mobile/data/models/payload.dart';
2+
3+
class NextTrade implements Payload {
4+
final String key;
5+
final int index;
6+
7+
NextTrade({required this.key, required this.index});
8+
9+
@override
10+
String get type => 'next_trade';
11+
12+
@override
13+
Map<String, dynamic> toJson() {
14+
return {
15+
type: {'key': key, 'index': index},
16+
};
17+
}
18+
19+
factory NextTrade.fromJson(Map<String, dynamic> json) {
20+
return NextTrade(
21+
key: json['key'] as String,
22+
index: json['index'] as int,
23+
);
24+
}
25+
26+
}

lib/data/models/payload.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:mostro_mobile/data/models/cant_do.dart';
22
import 'package:mostro_mobile/data/models/dispute.dart';
3+
import 'package:mostro_mobile/data/models/next_trade.dart';
34
import 'package:mostro_mobile/data/models/order.dart';
5+
import 'package:mostro_mobile/data/models/payment_failed.dart';
46
import 'package:mostro_mobile/data/models/payment_request.dart';
57
import 'package:mostro_mobile/data/models/peer.dart';
68
import 'package:mostro_mobile/data/models/rating_user.dart';
@@ -22,6 +24,10 @@ abstract class Payload {
2224
return Dispute.fromJson(json);
2325
} else if (json.containsKey('rating_user')) {
2426
return RatingUser.fromJson(json['rating_user']);
27+
} else if (json.containsKey('payment_failed')) {
28+
return PaymentFailed.fromJson(json['payment_failed']);
29+
} else if (json.containsKey('next_trade')) {
30+
return NextTrade.fromJson(json['next_trade']);
2531
} else {
2632
throw UnsupportedError('Unknown payload type');
2733
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import 'package:mostro_mobile/data/models/payload.dart';
2+
3+
class PaymentFailed implements Payload {
4+
final int paymentAttempts;
5+
final int paymentRetriesInterval;
6+
7+
PaymentFailed({
8+
required this.paymentAttempts,
9+
required this.paymentRetriesInterval,
10+
});
11+
12+
factory PaymentFailed.fromJson(Map<String, dynamic> json) {
13+
return PaymentFailed(
14+
paymentAttempts: json['payment_attempts'] as int,
15+
paymentRetriesInterval: json['payment_retries_interval'] as int,
16+
);
17+
}
18+
19+
@override
20+
String get type => 'payment_failed';
21+
22+
@override
23+
Map<String, dynamic> toJson() {
24+
return {
25+
type: {
26+
'payment_attempts': paymentAttempts,
27+
'payment_retries_interval': paymentRetriesInterval,
28+
},
29+
};
30+
}
31+
}

lib/features/order/models/order_state.dart

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class OrderState {
1010
final CantDo? cantDo;
1111
final Dispute? dispute;
1212
final Peer? peer;
13+
final PaymentFailed? paymentFailed;
1314
final _logger = Logger();
1415

1516
OrderState({
@@ -20,6 +21,7 @@ class OrderState {
2021
this.cantDo,
2122
this.dispute,
2223
this.peer,
24+
this.paymentFailed,
2325
});
2426

2527
factory OrderState.fromMostroMessage(MostroMessage message) {
@@ -31,12 +33,13 @@ class OrderState {
3133
cantDo: message.getPayload<CantDo>(),
3234
dispute: message.getPayload<Dispute>(),
3335
peer: message.getPayload<Peer>(),
36+
paymentFailed: message.getPayload<PaymentFailed>(),
3437
);
3538
}
3639

3740
@override
3841
String toString() =>
39-
'OrderState(status: $status, action: $action, order: $order, paymentRequest: $paymentRequest, cantDo: $cantDo, dispute: $dispute, peer: $peer)';
42+
'OrderState(status: $status, action: $action, order: $order, paymentRequest: $paymentRequest, cantDo: $cantDo, dispute: $dispute, peer: $peer, paymentFailed: $paymentFailed)';
4043

4144
@override
4245
bool operator ==(Object other) =>
@@ -48,6 +51,7 @@ class OrderState {
4851
other.paymentRequest == paymentRequest &&
4952
other.cantDo == cantDo &&
5053
other.dispute == dispute &&
54+
other.paymentFailed == paymentFailed &&
5155
other.peer == peer;
5256

5357
@override
@@ -59,6 +63,7 @@ class OrderState {
5963
cantDo,
6064
dispute,
6165
peer,
66+
paymentFailed,
6267
);
6368

6469
OrderState copyWith({
@@ -69,6 +74,7 @@ class OrderState {
6974
CantDo? cantDo,
7075
Dispute? dispute,
7176
Peer? peer,
77+
PaymentFailed? paymentFailed,
7278
}) {
7379
return OrderState(
7480
status: status ?? this.status,
@@ -78,6 +84,7 @@ class OrderState {
7884
cantDo: cantDo ?? this.cantDo,
7985
dispute: dispute ?? this.dispute,
8086
peer: peer ?? this.peer,
87+
paymentFailed: paymentFailed ?? this.paymentFailed,
8188
);
8289
}
8390

@@ -135,6 +142,7 @@ class OrderState {
135142
cantDo: message.getPayload<CantDo>() ?? cantDo,
136143
dispute: message.getPayload<Dispute>() ?? dispute,
137144
peer: newPeer,
145+
paymentFailed: message.getPayload<PaymentFailed>() ?? paymentFailed,
138146
);
139147

140148
_logger.i('New state: ${newState.status} - ${newState.action}');
@@ -154,7 +162,14 @@ class OrderState {
154162

155163
// Actions that should set status to waiting-buyer-invoice
156164
case Action.waitingBuyerInvoice:
165+
return Status.waitingBuyerInvoice;
166+
157167
case Action.addInvoice:
168+
// If current status is paymentFailed, maintain it for UI consistency
169+
// Otherwise, transition to waitingBuyerInvoice for normal flow
170+
if (status == Status.paymentFailed) {
171+
return Status.paymentFailed;
172+
}
158173
return Status.waitingBuyerInvoice;
159174

160175
// FIX: Cuando alguien toma una orden, debe cambiar el status inmediatamente
@@ -213,9 +228,12 @@ class OrderState {
213228
case Action.adminSettled:
214229
return Status.settledByAdmin;
215230

231+
// Actions that should set status to payment failed
232+
case Action.paymentFailed:
233+
return Status.paymentFailed;
234+
216235
// Informational actions that should preserve current status
217236
case Action.rateUser:
218-
case Action.paymentFailed:
219237
case Action.invoiceUpdated:
220238
case Action.sendDm:
221239
case Action.tradePubkey:
@@ -274,6 +292,12 @@ class OrderState {
274292
Action.cancel,
275293
],
276294
},
295+
Status.paymentFailed: {
296+
Action.paymentFailed: [
297+
// Only allow payment retry, no cancel or dispute during retrying
298+
Action.payInvoice,
299+
],
300+
},
277301
Status.active: {
278302
Action.buyerTookOrder: [
279303
Action.cancel,
@@ -361,6 +385,12 @@ class OrderState {
361385
Action.release,
362386
],
363387
},
388+
Status.settledHoldInvoice: {
389+
Action.addInvoice: [
390+
Action.addInvoice,
391+
Action.cancel,
392+
],
393+
},
364394
},
365395
Role.buyer: {
366396
Status.pending: {
@@ -391,6 +421,13 @@ class OrderState {
391421
Action.cancel,
392422
],
393423
},
424+
Status.paymentFailed: {
425+
Action.addInvoice: [
426+
// Only allow add invoice, no cancel or dispute during retrying
427+
Action.addInvoice,
428+
],
429+
Action.paymentFailed: [],
430+
},
394431
Status.active: {
395432
Action.holdInvoicePaymentAccepted: [
396433
Action.fiatSent,
@@ -473,6 +510,12 @@ class OrderState {
473510
Action.cancel,
474511
],
475512
},
513+
Status.settledHoldInvoice: {
514+
Action.addInvoice: [
515+
Action.addInvoice,
516+
Action.cancel,
517+
],
518+
},
476519
},
477520
};
478521
}

lib/features/order/notfiers/abstract_mostro_notifier.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,10 @@ class AbstractMostroNotifier extends StateNotifier<OrderState> {
213213
notifProvider.showInformation(event.action, values: {});
214214
break;
215215
case Action.paymentFailed:
216+
final paymentFailed = event.getPayload<PaymentFailed>();
216217
notifProvider.showInformation(event.action, values: {
217-
'payment_attempts': -1,
218-
'payment_retries_interval': -1000
218+
'payment_attempts': paymentFailed?.paymentAttempts,
219+
'payment_retries_interval': paymentFailed?.paymentRetriesInterval,
219220
});
220221
break;
221222
case Action.timeoutReversal:

lib/features/trades/screens/trade_detail_screen.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,11 @@ class TradeDetailScreen extends ConsumerWidget {
503503
widgets.add(_buildContactButton(context));
504504
break;
505505

506+
case actions.Action.paymentFailed:
507+
// Payment failed - Mostro is still retrying, only show Close button
508+
// No additional buttons (Add Invoice, Cancel, Dispute) should appear
509+
break;
510+
506511
case actions.Action.holdInvoicePaymentCanceled:
507512
case actions.Action.buyerInvoiceAccepted:
508513
case actions.Action.waitingSellerToPay:
@@ -514,7 +519,6 @@ class TradeDetailScreen extends ConsumerWidget {
514519
case actions.Action.adminAddSolver:
515520
case actions.Action.adminTakeDispute:
516521
case actions.Action.adminTookDispute:
517-
case actions.Action.paymentFailed:
518522
case actions.Action.invoiceUpdated:
519523
case actions.Action.tradePubkey:
520524
case actions.Action.cantDo:

lib/features/trades/widgets/mostro_message_detail_widget.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ class MostroMessageDetail extends ConsumerWidget {
8686
?.expirationSeconds ??
8787
900;
8888
final expMinutes = (expSecs / 60).round();
89+
// Check if we're in payment-failed state to show different message
90+
if (tradeState.status == Status.paymentFailed) {
91+
return S.of(context)!.addInvoicePaymentFailed(
92+
orderPayload?.amount.toString() ?? '',
93+
orderPayload?.fiatAmount.toString() ?? '',
94+
orderPayload?.fiatCode ?? '',
95+
);
96+
}
8997
return S.of(context)!.addInvoice(
9098
orderPayload?.amount.toString() ?? '',
9199
expMinutes,
@@ -186,7 +194,12 @@ class MostroMessageDetail extends ConsumerWidget {
186194
case actions.Action.adminSettled:
187195
return S.of(context)!.adminSettledUsers(orderPayload!.id ?? '');
188196
case actions.Action.paymentFailed:
189-
return S.of(context)!.paymentFailed('{attempts}', '{retries}');
197+
final payload = ref.read(orderNotifierProvider(orderId)).paymentFailed;
198+
final intervalInMinutes = ((payload?.paymentRetriesInterval ?? 0) / 60).round();
199+
return S.of(context)!.paymentFailed(
200+
payload?.paymentAttempts ?? 0,
201+
intervalInMinutes,
202+
);
190203
case actions.Action.invoiceUpdated:
191204
return S.of(context)!.invoiceUpdated;
192205
case actions.Action.holdInvoicePaymentCanceled:

0 commit comments

Comments
 (0)