Skip to content

Commit

Permalink
Merge pull request #1814 from get10101/feat/error-response
Browse files Browse the repository at this point in the history
feat: show error message details in dialog
  • Loading branch information
bonomat authored Jan 15, 2024
2 parents 2d95da9 + 2bf6fd8 commit a5cd1e9
Show file tree
Hide file tree
Showing 12 changed files with 354 additions and 118 deletions.
59 changes: 57 additions & 2 deletions mobile/lib/features/trade/domain/order.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,58 @@ enum OrderState {
}
}

enum FailureReasonType {
protocolError,
failed,
timeout,
rejected,
unknown;
}

class FailureReason {
final String? details;
final FailureReasonType failureType;

const FailureReason._({this.details, required this.failureType});

static const FailureReason standardProtocolError = FailureReason._(
failureType: FailureReasonType.protocolError, details: "Failed executing the DLC protocol.");
static const FailureReason failed = FailureReason._(
failureType: FailureReasonType.failed, details: "We failed processing the order.");
static const FailureReason timeout = FailureReason._(
failureType: FailureReasonType.timeout,
details: "The order timed out before finding a match");
static const FailureReason rejected =
FailureReason._(failureType: FailureReasonType.rejected, details: "The order was rejected.");
static const FailureReason unknown = FailureReason._(
failureType: FailureReasonType.unknown, details: "An unknown error occurred.");

static FailureReason? fromApi(bridge.FailureReason? failureReason) {
if (failureReason == null) {
return null;
}
switch (failureReason) {
case bridge.FailureReason_FailedToSetToFilling():
case bridge.FailureReason_TradeRequest():
case bridge.FailureReason_NodeAccess():
case bridge.FailureReason_NoUsableChannel():
case bridge.FailureReason_CollabRevert():
case bridge.FailureReason_OrderNotAcceptable():
case bridge.FailureReason_InvalidDlcOffer():
return standardProtocolError;
case bridge.FailureReason_TradeResponse():
return FailureReason._(
failureType: FailureReasonType.protocolError, details: failureReason.field0);
case bridge.FailureReason_TimedOut():
return timeout;
case bridge.FailureReason_OrderRejected():
return rejected;
case bridge.FailureReason_Unknown():
return unknown;
}
}
}

enum OrderType {
market;

Expand All @@ -68,6 +120,7 @@ class Order {
final double? executionPrice;
final DateTime creationTimestamp;
final OrderReason reason;
final FailureReason? failureReason;

Order(
{required this.id,
Expand All @@ -79,7 +132,8 @@ class Order {
required this.type,
required this.creationTimestamp,
this.executionPrice,
required this.reason});
required this.reason,
required this.failureReason});

static Order fromApi(bridge.Order order) {
return Order(
Expand All @@ -92,7 +146,8 @@ class Order {
type: OrderType.fromApi(order.orderType),
executionPrice: order.executionPrice,
creationTimestamp: DateTime.fromMillisecondsSinceEpoch(order.creationTimestamp * 1000),
reason: OrderReason.fromApi(order.reason));
reason: OrderReason.fromApi(order.reason),
failureReason: FailureReason.fromApi(order.failureReason));
}

static bridge.Order apiDummy() {
Expand Down
2 changes: 2 additions & 0 deletions mobile/lib/features/trade/submit_order_change_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class PendingOrder {
final PositionAction positionAction;
String? id;
Amount? pnl;
FailureReason? failureReason;

PendingOrder(this._tradeValues, this.positionAction, this.pnl);

Expand Down Expand Up @@ -81,6 +82,7 @@ class SubmitOrderChangeNotifier extends ChangeNotifier implements Subscriber {
_pendingOrder!.state = PendingOrderState.orderFailed;
break;
}
_pendingOrder!.failureReason = order.failureReason;

notifyListeners();
}
Expand Down
219 changes: 174 additions & 45 deletions mobile/lib/features/trade/trade_dialog.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import 'dart:convert';
import 'dart:io';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_10101/common/domain/background_task.dart';
import 'package:get_10101/common/domain/model.dart';
import 'package:get_10101/common/global_keys.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/task_status_dialog.dart';
import 'package:get_10101/common/value_data_row.dart';
import 'package:get_10101/features/trade/domain/trade_values.dart';
import 'package:get_10101/features/trade/submit_order_change_notifier.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:social_share/social_share.dart';
import 'package:url_launcher/url_launcher.dart';

class TradeDialog extends StatelessWidget {
const TradeDialog({super.key});
Expand All @@ -32,7 +38,7 @@ class TradeDialog extends StatelessWidget {
case PendingOrderState.orderFilled:
return TaskStatusDialog(title: "Fill Order", status: TaskStatus.success, content: body);
case PendingOrderState.orderFailed:
return TaskStatusDialog(title: "Fill Order", status: TaskStatus.failed, content: body);
return TaskStatusDialog(title: "Order", status: TaskStatus.failed, content: body);
}
}
}
Expand Down Expand Up @@ -63,55 +69,66 @@ Widget createSubmitWidget(
break;
}

Column body = Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 200,
child: Wrap(
runSpacing: 10,
children: [
pendingOrder.positionAction == PositionAction.close
? ValueDataRow(type: ValueType.amount, value: pendingOrder.pnl, label: pnlText)
: ValueDataRow(
type: ValueType.amount, value: pendingOrderValues?.margin, label: "Margin"),
ValueDataRow(
type: ValueType.amount, value: pendingOrderValues?.fee ?? Amount(0), label: "Fee")
],
),
),
Padding(
padding: const EdgeInsets.only(top: 20, left: 10, right: 10, bottom: 5),
child: Text(bottomText, style: const TextStyle(fontSize: 15)),
),
],
);

// Add "Do not close the app" while order is pending
if (pendingOrder.state == PendingOrderState.submitting ||
pendingOrder.state == PendingOrderState.submittedSuccessfully) {
body.children.add(
const Padding(
padding: EdgeInsets.only(left: 10, right: 10, bottom: 5),
child: Text("Do not close the app!",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
List<Widget> children = [];
if (pendingOrder.failureReason != null) {
children.add(
ErrorDetails(
details: pendingOrder.failureReason!.details ?? "unknown error",
),
);
}
} else {
children.addAll(
[
SizedBox(
width: 200,
child: Wrap(
runSpacing: 10,
children: [
pendingOrder.positionAction == PositionAction.close
? ValueDataRow(type: ValueType.amount, value: pendingOrder.pnl, label: pnlText)
: ValueDataRow(
type: ValueType.amount, value: pendingOrderValues?.margin, label: "Margin"),
ValueDataRow(
type: ValueType.amount, value: pendingOrderValues?.fee ?? Amount(0), label: "Fee")
],
),
),
Padding(
padding: const EdgeInsets.only(top: 20, left: 10, right: 10, bottom: 5),
child: Text(bottomText, style: const TextStyle(fontSize: 15)),
),
],
);

// Only display "share on twitter" when order is filled
if (pendingOrder.state == PendingOrderState.orderFilled) {
body.children.add(Padding(
padding: const EdgeInsets.only(top: 20, left: 10, right: 10, bottom: 5),
child: ElevatedButton(
onPressed: () async {
await shareTweet(pendingOrder.positionAction);
},
child: const Text("Share on Twitter")),
));
// Add "Do not close the app" while order is pending
if (pendingOrder.state == PendingOrderState.submitting ||
pendingOrder.state == PendingOrderState.submittedSuccessfully) {
children.add(
const Padding(
padding: EdgeInsets.only(left: 10, right: 10, bottom: 5),
child: Text("Do not close the app!",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
),
);
}

// Only display "share on twitter" when order is filled
if (pendingOrder.state == PendingOrderState.orderFilled) {
children.add(Padding(
padding: const EdgeInsets.only(top: 20, left: 10, right: 10, bottom: 5),
child: ElevatedButton(
onPressed: () async {
await shareTweet(pendingOrder.positionAction);
},
child: const Text("Share on Twitter")),
));
}
}

return body;
return Column(
mainAxisSize: MainAxisSize.min,
children: children,
);
}

Future<void> shareTweet(PositionAction action) async {
Expand All @@ -125,3 +142,115 @@ Future<void> shareTweet(PositionAction action) async {
await Share.share(shareText);
}
}

class ClickableHelpText extends StatelessWidget {
const ClickableHelpText({super.key});

@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
text: 'Please help us fix this issue and join our telegram group: ',
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: 'https://t.me/get10101',
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
final httpsUri = Uri(scheme: 'https', host: 't.me', path: 'get10101');
if (await canLaunchUrl(httpsUri)) {
await launchUrl(httpsUri, mode: LaunchMode.externalApplication);
} else {
showSnackBar(ScaffoldMessenger.of(rootNavigatorKey.currentContext!),
"Failed to open link");
}
},
),
],
),
);
}
}

// Returns a formatted json string if the provided argument is json, else, returns the argument
String getPrettyJSONString(String jsonObjectString) {
try {
var jsonObject = json.decode(jsonObjectString);
var encoder = const JsonEncoder.withIndent(" ");
return encoder.convert(jsonObject);
} catch (error) {
return jsonObjectString;
}
}

class ErrorDetails extends StatelessWidget {
final String details;

const ErrorDetails({super.key, required this.details});

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 20, left: 10, right: 10, bottom: 5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Error details:",
style: TextStyle(fontSize: 15),
),
Padding(
padding: const EdgeInsets.all(5.0),
child: SizedBox.square(
child: Container(
padding: const EdgeInsets.fromLTRB(5, 25, 5, 10.0),
color: Colors.grey.shade300,
child: Column(
children: [
Text(
getPrettyJSONString(details),
style: const TextStyle(fontSize: 15),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
GestureDetector(
child: const Icon(Icons.content_copy, size: 16),
onTap: () {
Clipboard.setData(ClipboardData(text: details)).then((_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Copied to clipboard"),
),
);
});
},
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
),
child: GestureDetector(
child: const Icon(Icons.share, size: 16),
onTap: () => Share.share(details),
),
)
],
),
],
),
),
),
),
const ClickableHelpText(),
],
),
);
}
}
2 changes: 1 addition & 1 deletion mobile/lib/features/wallet/wallet_history_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ class TransactionIdText extends StatelessWidget {
Text(truncateWithEllipsis(10, txId)),
IconButton(
padding: EdgeInsets.zero,
onPressed: () => launchUrl(uri),
onPressed: () => launchUrl(uri, mode: LaunchMode.externalApplication),
icon: const Icon(Icons.open_in_new, size: 18))
],
);
Expand Down
Loading

0 comments on commit a5cd1e9

Please sign in to comment.