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
12 changes: 12 additions & 0 deletions lib/core/app_routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:mostro_mobile/features/trades/screens/trade_detail_screen.dart';
import 'package:mostro_mobile/features/trades/screens/trades_screen.dart';
import 'package:mostro_mobile/features/relays/relays_screen.dart';
import 'package:mostro_mobile/features/order/screens/add_lightning_invoice_screen.dart';
import 'package:mostro_mobile/features/order/screens/pay_bond_invoice_screen.dart';
import 'package:mostro_mobile/features/order/screens/pay_lightning_invoice_screen.dart';
import 'package:mostro_mobile/features/order/screens/take_order_screen.dart';
import 'package:mostro_mobile/features/walkthrough/screens/walkthrough_screen.dart';
Expand Down Expand Up @@ -285,6 +286,17 @@ GoRouter createRouter(WidgetRef ref) {
),
),
),
GoRoute(
path: '/pay_bond/:orderId',
pageBuilder: (context, state) =>
buildPageWithDefaultTransition<void>(
context: context,
state: state,
child: PayBondInvoiceScreen(
orderId: state.pathParameters['orderId']!,
),
),
),
GoRoute(
path: '/add_invoice/:orderId',
pageBuilder: (context, state) =>
Expand Down
1 change: 1 addition & 0 deletions lib/data/models/enums/action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ enum Action {
takeSell('take-sell'),
takeBuy('take-buy'),
payInvoice('pay-invoice'),
payBondInvoice('pay-bond-invoice'),
fiatSent('fiat-sent'),
fiatSentOk('fiat-sent-ok'),
release('release'),
Expand Down
1 change: 1 addition & 0 deletions lib/data/models/enums/status.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ enum Status {
success('success'),
waitingBuyerInvoice('waiting-buyer-invoice'),
waitingPayment('waiting-payment'),
waitingTakerBond('waiting-taker-bond'),
paymentFailed('payment-failed'),
cooperativelyCanceled('cooperatively-canceled'),
inProgress('in-progress');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class NotificationMessageMapper {
case mostro.Action.takeSell:
return 'notification_order_taken_title';
case mostro.Action.payInvoice:
case mostro.Action.payBondInvoice:
return 'notification_payment_required_title';
case mostro.Action.fiatSent:
return 'notification_fiat_sent_title';
Expand Down Expand Up @@ -119,6 +120,7 @@ class NotificationMessageMapper {
case mostro.Action.takeSell:
return 'notification_buy_order_taken_message';
case mostro.Action.payInvoice:
case mostro.Action.payBondInvoice:
return 'notification_payment_required_message';
case mostro.Action.fiatSent:
return 'notification_fiat_sent_message';
Expand Down
3 changes: 3 additions & 0 deletions lib/features/notifications/widgets/notification_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ class NotificationItem extends ConsumerWidget {
case mostro_action.Action.addInvoice:
context.push('/add_invoice/${notification.orderId}');
break;
case mostro_action.Action.payBondInvoice:
context.push('/pay_bond/${notification.orderId}');
break;
case mostro_action.Action.canceled:
case mostro_action.Action.adminCanceled:
context.push('/order_book');
Expand Down
16 changes: 16 additions & 0 deletions lib/features/order/models/order_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ class OrderState {
case Action.payInvoice:
return Status.waitingPayment;

// Action that should set status to waiting-taker-bond (Phase 1.5 anti-abuse bond)
case Action.payBondInvoice:
return Status.waitingTakerBond;

// Actions that should set status to waiting-buyer-invoice
case Action.waitingBuyerInvoice:
return Status.waitingBuyerInvoice;
Expand Down Expand Up @@ -385,6 +389,12 @@ class OrderState {
Action.cancel,
],
},
Status.waitingTakerBond: {
Action.payBondInvoice: [
Action.payBondInvoice,
Action.cancel,
],
},
Status.waitingPayment: {
Action.payInvoice: [
Action.payInvoice,
Expand Down Expand Up @@ -529,6 +539,12 @@ class OrderState {
Action.cancel,
],
},
Status.waitingTakerBond: {
Action.payBondInvoice: [
Action.payBondInvoice,
Action.cancel,
],
},
Status.waitingPayment: {
Action.waitingSellerToPay: [
Action.cancel,
Expand Down
7 changes: 7 additions & 0 deletions lib/features/order/notifiers/abstract_mostro_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,13 @@ class AbstractMostroNotifier extends StateNotifier<OrderState> {
ref.read(sessionNotifierProvider.notifier).saveSession(session);
break;

case Action.payBondInvoice:
if (event.payload is PaymentRequest) {
navProvider.go('/pay_bond/${event.id!}');
}
ref.read(sessionNotifierProvider.notifier).saveSession(session);
break;

case Action.addInvoice:
final sessionNotifier = ref.read(sessionNotifierProvider.notifier);
sessionNotifier.saveSession(session);
Expand Down
235 changes: 235 additions & 0 deletions lib/features/order/screens/pay_bond_invoice_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:mostro_mobile/core/app_theme.dart';
import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart';
import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart';
import 'package:mostro_mobile/generated/l10n.dart';
import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';

class PayBondInvoiceScreen extends ConsumerWidget {
final String orderId;

const PayBondInvoiceScreen({super.key, required this.orderId});

Future<void> _confirmAndCancel(
BuildContext context,
WidgetRef ref,
) async {
final s = S.of(context)!;
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
backgroundColor: AppTheme.backgroundCard,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.white.withValues(alpha: 0.1)),
),
title: Text(
s.cancelTradeDialogTitle,
style: const TextStyle(
color: AppTheme.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
content: Text(
s.areYouSureCancel,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
height: 1.5,
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(
s.no,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.activeColor,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
child: Text(
s.yes,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
],
),
);

if (confirmed != true) return;
if (!context.mounted) return;

final orderNotifier = ref.read(orderNotifierProvider(orderId).notifier);
context.go('/');
await orderNotifier.cancelOrder();
Comment thread
Catrya marked this conversation as resolved.
}

Future<void> _shareInvoice(BuildContext context, String lnInvoice) async {
final messenger = ScaffoldMessenger.of(context);
final mediaQuery = MediaQuery.of(context);
final errorMessage = S.of(context)!.failedToShareInvoice;

try {
final uri = Uri.parse('lightning:$lnInvoice');
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
logger.i('Launched Lightning wallet with bond invoice');
} else {
await Share.share(lnInvoice);
logger.i('Shared bond invoice via share sheet');
}
} catch (e) {
logger.e('Failed to share bond invoice: $e');
SnackBarHelper.showTopSnackBarAsync(
messenger: messenger,
screenHeight: mediaQuery.size.height,
statusBarHeight: mediaQuery.padding.top,
message: errorMessage,
duration: const Duration(seconds: 3),
);
}
}

@override
Widget build(BuildContext context, WidgetRef ref) {
final s = S.of(context)!;
final orderState = ref.watch(orderNotifierProvider(orderId));
final lnInvoice = orderState.paymentRequest?.lnInvoice ?? '';
final bondAmount = orderState.paymentRequest?.order?.amount;

return Scaffold(
backgroundColor: AppTheme.dark1,
appBar: OrderAppBar(title: s.bondScreenTitle),
body: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(
16,
16,
16,
16 + MediaQuery.of(context).viewPadding.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
s.bondExplanation,
style: const TextStyle(
color: AppTheme.cream1,
fontSize: 15,
height: 1.4,
),
),
if (bondAmount != null && bondAmount > 0) ...[
const SizedBox(height: 16),
Text(
s.bondPayInvoicePrompt(bondAmount),
style: const TextStyle(
color: AppTheme.cream1,
fontSize: 15,
height: 1.4,
fontWeight: FontWeight.w500,
),
),
],
const SizedBox(height: 20),
Center(
child: Container(
padding: const EdgeInsets.all(8.0),
color: AppTheme.cream1,
child: QrImageView(
data: lnInvoice,
version: QrVersions.auto,
size: 250.0,
backgroundColor: AppTheme.cream1,
errorStateBuilder: (cxt, err) {
return Center(
child: Text(
s.failedToGenerateQR,
textAlign: TextAlign.center,
),
);
},
),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: lnInvoice.isEmpty
? null
: () {
Clipboard.setData(ClipboardData(text: lnInvoice));
logger.i('Copied bond invoice to clipboard');
SnackBarHelper.showTopSnackBar(
context,
s.invoiceCopiedToClipboard,
duration: const Duration(seconds: 2),
);
},
icon: const Icon(Icons.copy),
label: Text(s.copy),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.mostroGreen,
),
),
ElevatedButton.icon(
onPressed: lnInvoice.isEmpty
? null
: () => _shareInvoice(context, lnInvoice),
icon: const Icon(Icons.share),
label: Text(s.share),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.mostroGreen,
),
),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _confirmAndCancel(context, ref),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.red,
),
child: Text(s.cancel),
),
],
),
],
),
),
);
}
}
3 changes: 3 additions & 0 deletions lib/features/restore/restore_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,9 @@ class RestoreService {
return userRole == Role.seller
? Action.payInvoice
: Action.waitingSellerToPay;
case Status.waitingTakerBond:
// Order is in the anti-abuse bond payment window; taker pays the bond
return Action.payBondInvoice;
case Status.active:
// If user is buyer, they need to confirm fiat sent
// If user is seller, buyer took the order and seller waits
Expand Down
12 changes: 12 additions & 0 deletions lib/features/trades/screens/trade_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,18 @@ class TradeDetailScreen extends ConsumerWidget {
}
break;

case actions.Action.payBondInvoice:
final hasPaymentRequest = tradeState.paymentRequest != null;
if (hasPaymentRequest) {
widgets.add(_buildNostrButton(
S.of(context)!.payBondButton,
action: actions.Action.payBondInvoice,
backgroundColor: AppTheme.mostroGreen,
onPressed: () => context.push('/pay_bond/$orderId'),
));
}
break;

case actions.Action.addInvoice:
if (userRole == Role.buyer) {
widgets.add(_buildNostrButton(
Expand Down
Loading
Loading