Skip to content

Commit

Permalink
Add SatscardBalancePage
Browse files Browse the repository at this point in the history
- Used for displaying the balance of the active Satscard slot and allowing the user to sweep it into their wallet
  • Loading branch information
PeteClubSeven committed Feb 11, 2024
1 parent b55ecf1 commit 00ea63e
Show file tree
Hide file tree
Showing 5 changed files with 1,084 additions and 0 deletions.
156 changes: 156 additions & 0 deletions lib/routes/satscard_balance/broadcast_slot_sweep_transaction.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import 'dart:typed_data';

import 'package:breez/bloc/account/account_actions.dart';
import 'package:breez/bloc/account/account_bloc.dart';
import 'package:breez/bloc/blocs_provider.dart';
import 'package:breez/bloc/satscard/satscard_actions.dart';
import 'package:breez/bloc/satscard/satscard_bloc.dart';
import 'package:breez/routes/satscard_balance/satscard_balance_page.dart';
import 'package:breez/services/breezlib/data/messages.pb.dart';
import 'package:breez/services/injector.dart';
import 'package:breez/widgets/back_button.dart' as backBtn;
import 'package:breez/widgets/error_dialog.dart';
import 'package:breez/widgets/flushbar.dart';
import 'package:breez/widgets/link_launcher.dart';
import 'package:breez/widgets/single_button_bottom_bar.dart';
import 'package:breez_translations/breez_translations_locales.dart';
import 'package:breez_translations/generated/breez_translations.dart';
import 'package:flutter/material.dart';

class BroadcastSlotSweepTransactionPage extends StatefulWidget {
final Function() onBack;
final Function() onDone;
final AddressInfo Function() getAddressInfo;
final Uint8List Function() getPrivateKey;
final RawSlotSweepTransaction Function() getTransaction;

const BroadcastSlotSweepTransactionPage(
{@required this.onBack,
@required this.onDone,
@required this.getAddressInfo,
@required this.getPrivateKey,
@required this.getTransaction});

@override
State<StatefulWidget> createState() =>
BroadcastSlotSweepTransactionPageState();
}

class BroadcastSlotSweepTransactionPageState
extends State<BroadcastSlotSweepTransactionPage> {
TransactionDetails _signedTransaction;
Future<void> _future;

@override
void didChangeDependencies() {
super.didChangeDependencies();
_broadcastTransaction(context);
}

@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _future,
builder: (context, snapshot) {
final themeData = Theme.of(context);
final texts = context.texts();
final showError = snapshot.hasError;
return Scaffold(
appBar: AppBar(
title: Text(
texts.satscard_broadcast_title,
style: themeData.appBarTheme.titleTextStyle,
),
leading: backBtn.BackButton(
onPressed: widget.onBack,
),
),
bottomNavigationBar:
!snapshot.hasError || _signedTransaction == null
? null
: Padding(
padding: const EdgeInsets.only(top: 10),
child: SingleButtonBottomBar(
stickToBottom: true,
text: texts.satscard_balance_button_retry_label,
onPressed: () => _broadcastTransaction(context),
),
),
body: showError
? buildErrorBody(
themeData, _getErrorText(texts, snapshot.error))
: buildLoaderBody(themeData, _getLoaderText(texts)),
);
});
}

String _getErrorText(BreezTranslations texts, Object error) {
return _signedTransaction == null
? texts.satscard_broadcast_error_signing(error)
: texts.satscard_broadcast_error_broadcasting(error);
}

String _getLoaderText(BreezTranslations texts) {
return _signedTransaction == null
? texts.satscard_broadcast_signing_label
: texts.satscard_broadcast_broadcasting_label;
}

void _broadcastTransaction(BuildContext context) {
if (!context.mounted) {
return;
}
setState(() {
_future = Future.sync(() {
// Sign the selected transaction if we haven't already
if (_signedTransaction == null) {
final satscardBloc = AppBlocsProvider.of<SatscardBloc>(context);
final info = widget.getAddressInfo();
final tx = widget.getTransaction();
final key = widget.getPrivateKey();
final action = SignSlotSweepTransaction(info, tx, key);
satscardBloc.actionsSink.add(action);
return action.future.then((result) {
if (context.mounted) {
setState(() {
_signedTransaction = result as TransactionDetails;
});
}
});
}
}).then((_) {
final accountBloc = AppBlocsProvider.of<AccountBloc>(context);
final action = PublishTransaction(_signedTransaction.tx);
accountBloc.userActionsSink.add(action);
return action.future;
}).then((_) {
if (context.mounted) {
final texts = context.texts();
final tx = _signedTransaction;

widget.onDone();
promptMessage(
context,
texts.satscard_broadcast_complete_title,
Builder(
builder: (context) => LinkLauncher(
linkName: tx.txHash,
linkAddress: "https://blockstream.info/tx/${tx.txHash}",
onCopy: () {
ServiceInjector().device.setClipboardText(tx.txHash);
showFlushbar(
context,
message: texts.add_funds_transaction_id_copied,
duration: const Duration(seconds: 3),
);
},
),
),
contentPadding:
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 32.0),
);
}
});
});
}
}
200 changes: 200 additions & 0 deletions lib/routes/satscard_balance/satscard_balance_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import 'dart:typed_data';

import 'package:auto_size_text/auto_size_text.dart';
import 'package:breez/bloc/account/account_model.dart';
import 'package:breez/routes/satscard_balance/broadcast_slot_sweep_transaction.dart';
import 'package:breez/routes/satscard_balance/slot_balance_page.dart';
import 'package:breez/routes/satscard_balance/sweep_slot_page.dart';
import 'package:breez/services/breezlib/data/messages.pb.dart';
import 'package:breez/services/injector.dart';
import 'package:breez/theme_data.dart' as theme;
import 'package:breez/utils/min_font_size.dart';
import 'package:breez/widgets/circular_progress.dart';
import 'package:breez/widgets/flushbar.dart';
import 'package:breez/widgets/warning_box.dart';
import 'package:breez_translations/generated/breez_translations.dart';
import 'package:cktap_protocol/cktapcard.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';

class SatscardBalancePage extends StatefulWidget {
final Satscard _card;
final Slot _slot;

const SatscardBalancePage(this._card, this._slot);

@override
State<StatefulWidget> createState() => SatscardBalancePageState();
}

class SatscardBalancePageState extends State<SatscardBalancePage> {
final _pageController = PageController();

AddressInfo _recentAddressInfo;
RawSlotSweepTransaction _selectedTransaction;
Uint8List _slotPrivateKey;

@override
void dispose() {
_pageController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: [
SlotBalancePage(
widget._card,
widget._slot,
onBack: () => Navigator.pop(context),
onSweep: (balance) {
_recentAddressInfo = balance;
_pageController.nextPage(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
},
),
SweepSlotPage(
widget._card,
widget._slot,
onBack: () => _pageController.previousPage(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
onUnsealed: (transaction, privateKey) {
_selectedTransaction = transaction;
_slotPrivateKey = privateKey;
_pageController.nextPage(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
},
getAddressInfo: () => _recentAddressInfo,
getCachedPrivateKey: () {
// Allow for unsealed slots
if (_slotPrivateKey != null && _slotPrivateKey.isEmpty) {
return widget._slot.privkey;
}
return _slotPrivateKey;
},
),
BroadcastSlotSweepTransactionPage(
onBack: () => _pageController.previousPage(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
onDone: () => Navigator.of(context).pop(),
getAddressInfo: () => _recentAddressInfo,
getPrivateKey: () => _slotPrivateKey,
getTransaction: () => _selectedTransaction,
),
],
),
);
}
}

Widget buildErrorBody(ThemeData themeData, String title) {
return Stack(
children: <Widget>[
Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
buildWarning(themeData, title: title),
],
),
),
],
);
}

Widget buildLoaderBody(ThemeData themeData, String title) {
return Stack(
children: <Widget>[
Positioned.fill(
child: buildIndicator(themeData, title: title),
),
],
);
}

Widget buildIndicator(ThemeData themeData, {String title}) {
return CircularProgress(
size: 64,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
color: themeData.progressIndicatorTheme.color,
title: title,
);
}

ListTile buildSlotPageTextTile(
BuildContext context,
MinFontSize minFont, {
String titleText,
Color titleColor,
String trailingText,
Color trailingColor,
String copyMessage,
}) {
final style = theme.FieldTextStyle.labelStyle
.copyWith(color: titleColor ?? Colors.white);
final trailing = AutoSizeText(
trailingText ?? "",
style: style.copyWith(color: trailingColor ?? Colors.white),
maxLines: 1,
minFontSize: minFont.minFontSize,
stepGranularity: 0.1,
);

return ListTile(
visualDensity: const VisualDensity(horizontal: 0, vertical: -4),
title: AutoSizeText(
titleText ?? "",
style: style,
maxLines: 1,
minFontSize: minFont.minFontSize,
stepGranularity: 0.1,
),
trailing: copyMessage == null
? trailing
: GestureDetector(
onTap: () {
ServiceInjector().device.setClipboardText(trailingText);
showFlushbar(context, message: copyMessage);
},
child: trailing,
),
);
}

Widget buildWarning(ThemeData themeData, {String title}) {
return WarningBox(
child: Text(
title,
style: themeData.textTheme.titleLarge,
textAlign: TextAlign.left,
),
);
}

String formatBalanceValue(
BreezTranslations texts, AccountModel acc, Int64 sats) {
if (acc == null) {
return sats.toString();
}
final satsString = acc.currency.format(sats);
if (acc.fiatCurrency == null || sats <= 0) {
return texts.satscard_balance_value_no_fiat(satsString);
} else {
final fiat = acc.fiatCurrency.format(sats);
return texts.satscard_balance_value_with_fiat(satsString, fiat);
}
}
Loading

0 comments on commit 00ea63e

Please sign in to comment.