mirror of
https://github.com/aljazceru/breez-sdk-liquid.git
synced 2025-12-21 16:04:27 +01:00
Update example app on Flutter plugin (#220)
* Update example app on Flutter plugin * Expose `empty_wallet_cache` through Dart bindings (#224)
This commit is contained in:
108
packages/flutter/example/lib/routes/connect/connect_page.dart
Normal file
108
packages/flutter/example/lib/routes/connect/connect_page.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
import 'package:bip39/bip39.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/connect/restore_page.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/home_page.dart';
|
||||
import 'package:flutter_breez_liquid_example/services/credentials_manager.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class ConnectPage extends StatefulWidget {
|
||||
final CredentialsManager credentialsManager;
|
||||
const ConnectPage({super.key, required this.credentialsManager});
|
||||
|
||||
@override
|
||||
State<ConnectPage> createState() => _ConnectPageState();
|
||||
}
|
||||
|
||||
class _ConnectPageState extends State<ConnectPage> {
|
||||
bool connecting = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Breez Liquid SDK Demo'),
|
||||
foregroundColor: Colors.blue,
|
||||
),
|
||||
body: Center(
|
||||
child: connecting
|
||||
? const CircularProgressIndicator(color: Colors.blue)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: ElevatedButton(
|
||||
child: const Text("Create new wallet"),
|
||||
onPressed: () async {
|
||||
await createWallet();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: ElevatedButton(
|
||||
child: const Text("Restore from backup"),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return RestorePage(
|
||||
onRestore: (mnemonic) async {
|
||||
return await createWallet(mnemonic: mnemonic);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Null> createWallet({String? mnemonic}) async {
|
||||
final walletMnemonic = mnemonic ??= generateMnemonic(strength: 128);
|
||||
debugPrint("${mnemonic.isEmpty ? "Creating" : "Restoring"} wallet with $walletMnemonic");
|
||||
return await initializeWallet(mnemonic: walletMnemonic).then(
|
||||
(liquidSDK) async {
|
||||
await widget.credentialsManager.storeMnemonic(mnemonic: walletMnemonic).then((_) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => HomePage(
|
||||
liquidSDK: liquidSDK,
|
||||
credentialsManager: widget.credentialsManager,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<BindingLiquidSdk> initializeWallet({
|
||||
required String mnemonic,
|
||||
Network network = Network.liquid,
|
||||
}) async {
|
||||
final dataDir = await getApplicationDocumentsDirectory();
|
||||
final req = ConnectRequest(
|
||||
mnemonic: mnemonic,
|
||||
dataDir: dataDir.path,
|
||||
network: network,
|
||||
);
|
||||
return await connect(req: req);
|
||||
}
|
||||
}
|
||||
104
packages/flutter/example/lib/routes/connect/restore_page.dart
Normal file
104
packages/flutter/example/lib/routes/connect/restore_page.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class RestorePage extends StatefulWidget {
|
||||
final Future Function(String mnemonic) onRestore;
|
||||
|
||||
const RestorePage({super.key, required this.onRestore});
|
||||
|
||||
@override
|
||||
State<RestorePage> createState() => _RestorePageState();
|
||||
}
|
||||
|
||||
class _RestorePageState extends State<RestorePage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
List<TextEditingController> textFieldControllers =
|
||||
List<TextEditingController>.generate(12, (_) => TextEditingController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final clipboardData = await Clipboard.getData('text/plain');
|
||||
final clipboardMnemonics = clipboardData?.text?.split(" ");
|
||||
if (clipboardMnemonics?.length == 12) {
|
||||
for (var i = 0; i < clipboardMnemonics!.length; i++) {
|
||||
textFieldControllers.elementAt(i).text = clipboardMnemonics.elementAt(i);
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.paste, color: Colors.blue),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: MediaQuery.of(context).size.width / 2,
|
||||
childAspectRatio: 2,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemCount: 12,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return TextFormField(
|
||||
decoration: InputDecoration(labelText: "${index + 1}"),
|
||||
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r"\s\b|\b\s"))],
|
||||
validator: (String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter value';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
controller: textFieldControllers[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 40.0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
elevation: 0.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
final mnemonic = textFieldControllers
|
||||
.map((controller) => controller.text.toLowerCase().trim())
|
||||
.toList()
|
||||
.join(" ");
|
||||
widget.onRestore(mnemonic);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
"RESTORE",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).primaryTextTheme.titleMedium,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
114
packages/flutter/example/lib/routes/home/home_page.dart
Normal file
114
packages/flutter/example/lib/routes/home/home_page.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/balance.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/bottom_app_bar.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/drawer.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/mnemonics_dialog.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/payment_list/payment_list.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/qr_scan_action_button.dart';
|
||||
import 'package:flutter_breez_liquid_example/services/credentials_manager.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
final CredentialsManager credentialsManager;
|
||||
final BindingLiquidSdk liquidSDK;
|
||||
|
||||
const HomePage({super.key, required this.credentialsManager, required this.liquidSDK});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
Stream<GetInfoResponse> walletInfoStream() async* {
|
||||
debugPrint("Initialized walletInfoStream");
|
||||
GetInfoRequest req = const GetInfoRequest(withScan: false);
|
||||
yield await widget.liquidSDK.getInfo(req: req);
|
||||
while (true) {
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
yield await widget.liquidSDK.getInfo(req: req);
|
||||
debugPrint("Refreshed wallet info");
|
||||
}
|
||||
}
|
||||
|
||||
Stream<List<Payment>> paymentsStream() async* {
|
||||
debugPrint("Initialized paymentsStream");
|
||||
yield await widget.liquidSDK.listPayments();
|
||||
while (true) {
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
yield await widget.liquidSDK.listPayments();
|
||||
debugPrint("Refreshed payments");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Breez Liquid SDK Demo'),
|
||||
titleTextStyle: const TextStyle(fontSize: 16.0, color: Colors.blue),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.blue,
|
||||
leading: Builder(
|
||||
builder: (context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [],
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: constraints.maxHeight * 0.3,
|
||||
color: Colors.white,
|
||||
child: Balance(walletInfoStream: walletInfoStream()),
|
||||
),
|
||||
Container(
|
||||
height: constraints.maxHeight * 0.7,
|
||||
color: Colors.white,
|
||||
child: PaymentList(
|
||||
paymentsStream: paymentsStream(),
|
||||
onRefresh: () async => await _sync(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
drawer: HomePageDrawer(liquidSDK: widget.liquidSDK, credentialsManager: widget.credentialsManager),
|
||||
floatingActionButton: QrActionButton(liquidSDK: widget.liquidSDK),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
||||
bottomNavigationBar: HomePageBottomAppBar(
|
||||
liquidSDK: widget.liquidSDK,
|
||||
paymentsStream: paymentsStream(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _sync() async {
|
||||
try {
|
||||
debugPrint("Syncing wallet.");
|
||||
await widget.liquidSDK.sync();
|
||||
debugPrint("Wallet synced!");
|
||||
} on Exception catch (e) {
|
||||
final errMsg = "Failed to sync wallet. $e";
|
||||
debugPrint(errMsg);
|
||||
if (context.mounted) {
|
||||
final snackBar = SnackBar(content: Text(errMsg));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
|
||||
|
||||
class Balance extends StatelessWidget {
|
||||
final Stream<GetInfoResponse> walletInfoStream;
|
||||
|
||||
const Balance({super.key, required this.walletInfoStream});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<GetInfoResponse>(
|
||||
stream: walletInfoStream,
|
||||
builder: (context, walletInfoSnapshot) {
|
||||
if (walletInfoSnapshot.hasError) {
|
||||
return Center(child: Text('Error: ${walletInfoSnapshot.error}'));
|
||||
}
|
||||
|
||||
if (!walletInfoSnapshot.hasData) {
|
||||
return const Center(child: Text('Loading...'));
|
||||
}
|
||||
|
||||
if (walletInfoSnapshot.requireData.balanceSat.isNaN) {
|
||||
return const Center(child: Text('No balance.'));
|
||||
}
|
||||
final walletInfo = walletInfoSnapshot.data!;
|
||||
|
||||
return Center(
|
||||
child: Text(
|
||||
"${walletInfo.balanceSat} sats",
|
||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(color: Colors.blue),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/receive_payment/receive_payment_dialog.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/send_payment/send_payment_dialog.dart';
|
||||
|
||||
class HomePageBottomAppBar extends StatelessWidget {
|
||||
final BindingLiquidSdk liquidSDK;
|
||||
final Stream<List<Payment>> paymentsStream;
|
||||
const HomePageBottomAppBar({super.key, required this.liquidSDK, required this.paymentsStream});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BottomAppBar(
|
||||
color: Colors.blue,
|
||||
height: 60,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => SendPaymentDialog(liquidSdk: liquidSDK),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"SEND",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).primaryTextTheme.titleMedium,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(width: 64),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
ReceivePaymentDialog(liquidSDK: liquidSDK, paymentsStream: paymentsStream),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"RECEIVE",
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
style: Theme.of(context).primaryTextTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
123
packages/flutter/example/lib/routes/home/widgets/drawer.dart
Normal file
123
packages/flutter/example/lib/routes/home/widgets/drawer.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/mnemonics_dialog.dart';
|
||||
import 'package:flutter_breez_liquid_example/services/credentials_manager.dart';
|
||||
|
||||
class HomePageDrawer extends StatefulWidget {
|
||||
final CredentialsManager credentialsManager;
|
||||
final BindingLiquidSdk liquidSDK;
|
||||
|
||||
const HomePageDrawer({super.key, required this.liquidSDK, required this.credentialsManager});
|
||||
|
||||
@override
|
||||
State<HomePageDrawer> createState() => _HomePageDrawerState();
|
||||
}
|
||||
|
||||
class _HomePageDrawerState extends State<HomePageDrawer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
backgroundColor: Colors.blue,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const DrawerHeader(
|
||||
curve: Curves.fastOutSlowIn,
|
||||
child: Text(
|
||||
'Breez Liquid SDK Demo',
|
||||
style: TextStyle(fontSize: 16.0, color: Colors.white),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
enabled: false,
|
||||
leading: const Icon(Icons.backup_outlined),
|
||||
title: const Text('Backup'),
|
||||
titleTextStyle:
|
||||
const TextStyle(fontSize: 16.0, color: Colors.white, decoration: TextDecoration.lineThrough),
|
||||
onTap: () async {
|
||||
try {
|
||||
debugPrint("Creating backup.");
|
||||
// TODO: Backup API should return backup file or it's filepath
|
||||
await widget.liquidSDK.backup();
|
||||
debugPrint("Created backup.");
|
||||
} catch (e) {
|
||||
final errMsg = "Failed to create backup. $e";
|
||||
debugPrint(errMsg);
|
||||
if (context.mounted) {
|
||||
final snackBar = SnackBar(content: Text(errMsg));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
enabled: false,
|
||||
leading: const Icon(Icons.restore),
|
||||
title: const Text('Restore'),
|
||||
titleTextStyle:
|
||||
const TextStyle(fontSize: 16.0, color: Colors.white, decoration: TextDecoration.lineThrough),
|
||||
onTap: () async {
|
||||
try {
|
||||
debugPrint("Restoring backup.");
|
||||
// TODO: Select backup file to restore
|
||||
RestoreRequest req = const RestoreRequest();
|
||||
await widget.liquidSDK.restore(req: req);
|
||||
debugPrint("Restored backup.");
|
||||
} catch (e) {
|
||||
final errMsg = "Failed to restore backup. $e";
|
||||
debugPrint(errMsg);
|
||||
if (context.mounted) {
|
||||
final snackBar = SnackBar(content: Text(errMsg));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.cached, color: Colors.white),
|
||||
title: const Text('Empty Wallet Cache'),
|
||||
titleTextStyle: const TextStyle(fontSize: 16.0, color: Colors.white),
|
||||
onTap: () async {
|
||||
try {
|
||||
debugPrint("Emptying wallet cache.");
|
||||
await widget.liquidSDK.emptyWalletCache();
|
||||
debugPrint("Emptied wallet cache.");
|
||||
} catch (e) {
|
||||
final errMsg = "Failed to empty wallet cache. $e";
|
||||
debugPrint(errMsg);
|
||||
if (context.mounted) {
|
||||
final snackBar = SnackBar(content: Text(errMsg));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline, color: Colors.white),
|
||||
title: const Text('Display Mnemonics'),
|
||||
titleTextStyle: const TextStyle(fontSize: 16.0, color: Colors.white),
|
||||
onTap: () async {
|
||||
try {
|
||||
await widget.credentialsManager.restoreMnemonic().then((mnemonics) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => MnemonicsDialog(
|
||||
mnemonics: mnemonics.split(" "),
|
||||
),
|
||||
);
|
||||
});
|
||||
} on Exception catch (e) {
|
||||
final errMsg = "Failed to display mnemonics. $e";
|
||||
debugPrint(errMsg);
|
||||
if (context.mounted) {
|
||||
final snackBar = SnackBar(content: Text(errMsg));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class MnemonicsDialog extends StatelessWidget {
|
||||
final List<String> mnemonics;
|
||||
|
||||
MnemonicsDialog({super.key, required this.mnemonics});
|
||||
|
||||
final textFieldControllers = List<TextEditingController>.generate(12, (_) => TextEditingController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("Mnemonics"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: MediaQuery.of(context).size.width / 2,
|
||||
childAspectRatio: 2,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemCount: mnemonics.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
textFieldControllers[index].text = mnemonics.elementAt(index);
|
||||
return TextField(
|
||||
readOnly: true,
|
||||
controller: textFieldControllers[index],
|
||||
decoration: InputDecoration(labelText: "${index + 1}", border: InputBorder.none),
|
||||
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r"\s\b|\b\s"))],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("CLOSE"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/payment_list/widgets/payment_item.dart';
|
||||
|
||||
class PaymentList extends StatelessWidget {
|
||||
final Future Function() onRefresh;
|
||||
final Stream<List<Payment>> paymentsStream;
|
||||
|
||||
const PaymentList({super.key, required this.paymentsStream, required this.onRefresh});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<List<Payment>>(
|
||||
stream: paymentsStream,
|
||||
builder: (context, paymentsSnapshot) {
|
||||
if (paymentsSnapshot.hasError) {
|
||||
return Text('Error: ${paymentsSnapshot.error}');
|
||||
}
|
||||
|
||||
if (!paymentsSnapshot.hasData) {
|
||||
return const Center(child: Text('Loading...'));
|
||||
}
|
||||
|
||||
if (paymentsSnapshot.requireData.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'You are ready to receive funds.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final paymentList = paymentsSnapshot.data!;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
debugPrint("Pulled to refresh");
|
||||
return await onRefresh();
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: paymentList.length,
|
||||
shrinkWrap: true,
|
||||
primary: true,
|
||||
itemBuilder: (BuildContext context, int index) => PaymentItem(item: paymentList[index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class PaymentItem extends StatelessWidget {
|
||||
final Payment item;
|
||||
const PaymentItem({super.key, required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
onLongPress: item.preimage != null
|
||||
? () {
|
||||
try {
|
||||
debugPrint("Store payment preimage on clipboard. Preimage: ${item.preimage!}");
|
||||
Clipboard.setData(ClipboardData(text: item.preimage!));
|
||||
const snackBar = SnackBar(content: Text('Copied payment preimage to clipboard.'));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
} catch (e) {
|
||||
final snackBar = SnackBar(
|
||||
content: Text('Failed to copy payment preimage to clipboard. $e'),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
: item.swapId != null
|
||||
? () {
|
||||
try {
|
||||
debugPrint("Store swap ID on clipboard. Swap ID: ${item.swapId!}");
|
||||
Clipboard.setData(ClipboardData(text: item.swapId!));
|
||||
const snackBar = SnackBar(content: Text('Copied swap ID to clipboard.'));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
} catch (e) {
|
||||
final snackBar = SnackBar(
|
||||
content: Text('Failed to copy payment preimage to clipboard. $e'),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
title: Text(_paymentTitle(item)),
|
||||
subtitle: Text(
|
||||
DateFormat('dd/MM/yyyy, HH:mm').format(
|
||||
DateTime.fromMillisecondsSinceEpoch(item.timestamp * 1000),
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${item.paymentType == PaymentType.send ? "-" : "+"}${item.amountSat} sats",
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (item.feesSat != null) ...[
|
||||
Text("FEE: ${item.feesSat} sats"),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _paymentTitle(Payment payment) {
|
||||
final paymentType = payment.paymentType;
|
||||
|
||||
switch (paymentType) {
|
||||
case PaymentType.receive:
|
||||
return "Received Payment";
|
||||
case PaymentType.send:
|
||||
return "Sent Payment";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/qr_scan/scan_overlay.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/qr_scan/scanner_error_widget.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class BarcodeScanner extends StatefulWidget {
|
||||
const BarcodeScanner({super.key});
|
||||
|
||||
@override
|
||||
State<BarcodeScanner> createState() => _BarcodeScannerState();
|
||||
}
|
||||
|
||||
class _BarcodeScannerState extends State<BarcodeScanner> with WidgetsBindingObserver {
|
||||
bool popped = false;
|
||||
|
||||
final MobileScannerController controller = MobileScannerController(
|
||||
autoStart: false,
|
||||
torchEnabled: false,
|
||||
useNewCameraSelector: true,
|
||||
);
|
||||
|
||||
Barcode? _barcode;
|
||||
StreamSubscription<Object?>? _subscription;
|
||||
|
||||
void _handleBarcode(BarcodeCapture barcodes) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_barcode = barcodes.barcodes.firstOrNull;
|
||||
});
|
||||
if (popped) {
|
||||
debugPrint("Skipping, already popped");
|
||||
return;
|
||||
}
|
||||
popped = true;
|
||||
final code = _barcode?.rawValue;
|
||||
if (code == null) {
|
||||
debugPrint("Failed to scan QR code.");
|
||||
} else {
|
||||
popped = true;
|
||||
debugPrint("Popping read QR code: $code");
|
||||
Navigator.of(context).pop(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_subscription = controller.barcodes.listen(_handleBarcode);
|
||||
|
||||
unawaited(controller.start());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (!controller.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case AppLifecycleState.detached:
|
||||
case AppLifecycleState.hidden:
|
||||
case AppLifecycleState.paused:
|
||||
return;
|
||||
case AppLifecycleState.resumed:
|
||||
_subscription = controller.barcodes.listen(_handleBarcode);
|
||||
|
||||
unawaited(controller.start());
|
||||
case AppLifecycleState.inactive:
|
||||
unawaited(_subscription?.cancel());
|
||||
_subscription = null;
|
||||
unawaited(controller.stop());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var scanWindowDimension = MediaQuery.of(context).size.width - 72;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
MobileScanner(
|
||||
scanWindow: Rect.fromCenter(
|
||||
center: MediaQuery.sizeOf(context).center(Offset.zero),
|
||||
width: scanWindowDimension,
|
||||
height: scanWindowDimension,
|
||||
),
|
||||
controller: controller,
|
||||
errorBuilder: (context, error, child) {
|
||||
return ScannerErrorWidget(error: error);
|
||||
},
|
||||
overlayBuilder: (context, constraints) {
|
||||
return const ScanOverlay();
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
unawaited(_subscription?.cancel());
|
||||
_subscription = null;
|
||||
super.dispose();
|
||||
await controller.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ScanOverlay extends StatelessWidget {
|
||||
const ScanOverlay({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var dimension = MediaQuery.of(context).size.width - 72;
|
||||
return Center(
|
||||
child: CustomPaint(
|
||||
painter: BorderPainter(),
|
||||
child: SizedBox(
|
||||
width: dimension,
|
||||
height: dimension,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BorderPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
const width = 4.0;
|
||||
const radius = 16.0;
|
||||
const tRadius = 2 * radius;
|
||||
final rect = Rect.fromLTWH(
|
||||
width,
|
||||
width,
|
||||
size.width - 2 * width,
|
||||
size.height - 2 * width,
|
||||
);
|
||||
final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(radius));
|
||||
const clippingRect0 = Rect.fromLTWH(
|
||||
0,
|
||||
0,
|
||||
tRadius,
|
||||
tRadius,
|
||||
);
|
||||
final clippingRect1 = Rect.fromLTWH(
|
||||
size.width - tRadius,
|
||||
0,
|
||||
tRadius,
|
||||
tRadius,
|
||||
);
|
||||
final clippingRect2 = Rect.fromLTWH(
|
||||
0,
|
||||
size.height - tRadius,
|
||||
tRadius,
|
||||
tRadius,
|
||||
);
|
||||
final clippingRect3 = Rect.fromLTWH(
|
||||
size.width - tRadius,
|
||||
size.height - tRadius,
|
||||
tRadius,
|
||||
tRadius,
|
||||
);
|
||||
|
||||
final path = Path()
|
||||
..addRect(clippingRect0)
|
||||
..addRect(clippingRect1)
|
||||
..addRect(clippingRect2)
|
||||
..addRect(clippingRect3);
|
||||
|
||||
canvas.clipPath(path);
|
||||
canvas.drawRRect(
|
||||
rrect,
|
||||
Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = width,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class StartStopMobileScannerButton extends StatelessWidget {
|
||||
const StartStopMobileScannerButton({required this.controller, super.key});
|
||||
|
||||
final MobileScannerController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: controller,
|
||||
builder: (context, state, child) {
|
||||
if (!state.isInitialized || !state.isRunning) {
|
||||
return IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
await controller.start();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.stop),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
await controller.stop();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SwitchCameraButton extends StatelessWidget {
|
||||
const SwitchCameraButton({required this.controller, super.key});
|
||||
|
||||
final MobileScannerController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: controller,
|
||||
builder: (context, state, child) {
|
||||
if (!state.isInitialized || !state.isRunning) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final int? availableCameras = state.availableCameras;
|
||||
|
||||
if (availableCameras != null && availableCameras < 2) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final Widget icon;
|
||||
|
||||
switch (state.cameraDirection) {
|
||||
case CameraFacing.front:
|
||||
icon = const Icon(Icons.camera_front);
|
||||
case CameraFacing.back:
|
||||
icon = const Icon(Icons.camera_rear);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
iconSize: 32.0,
|
||||
icon: icon,
|
||||
onPressed: () async {
|
||||
await controller.switchCamera();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToggleFlashlightButton extends StatelessWidget {
|
||||
const ToggleFlashlightButton({required this.controller, super.key});
|
||||
|
||||
final MobileScannerController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: controller,
|
||||
builder: (context, state, child) {
|
||||
if (!state.isInitialized || !state.isRunning) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
switch (state.torchState) {
|
||||
case TorchState.auto:
|
||||
return IconButton(
|
||||
color: Colors.white,
|
||||
iconSize: 32.0,
|
||||
icon: const Icon(Icons.flash_auto),
|
||||
onPressed: () async {
|
||||
await controller.toggleTorch();
|
||||
},
|
||||
);
|
||||
case TorchState.off:
|
||||
return IconButton(
|
||||
color: Colors.white,
|
||||
iconSize: 32.0,
|
||||
icon: const Icon(Icons.flash_off),
|
||||
onPressed: () async {
|
||||
await controller.toggleTorch();
|
||||
},
|
||||
);
|
||||
case TorchState.on:
|
||||
return IconButton(
|
||||
color: Colors.white,
|
||||
iconSize: 32.0,
|
||||
icon: const Icon(Icons.flash_on),
|
||||
onPressed: () async {
|
||||
await controller.toggleTorch();
|
||||
},
|
||||
);
|
||||
case TorchState.unavailable:
|
||||
return const Icon(
|
||||
Icons.no_flash,
|
||||
color: Colors.grey,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class ScannerErrorWidget extends StatelessWidget {
|
||||
const ScannerErrorWidget({super.key, required this.error});
|
||||
|
||||
final MobileScannerException error;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String errorMessage;
|
||||
|
||||
switch (error.errorCode) {
|
||||
case MobileScannerErrorCode.controllerUninitialized:
|
||||
errorMessage = 'Controller not ready.';
|
||||
case MobileScannerErrorCode.permissionDenied:
|
||||
errorMessage = 'Permission denied';
|
||||
case MobileScannerErrorCode.unsupported:
|
||||
errorMessage = 'Scanning is unsupported on this device';
|
||||
default:
|
||||
errorMessage = 'Generic Error';
|
||||
break;
|
||||
}
|
||||
|
||||
return ColoredBox(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: Icon(Icons.error, color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
errorMessage,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
error.errorDetails?.message ?? '',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/qr_scan/barcode_scanner_simple.dart';
|
||||
import 'package:flutter_breez_liquid_example/routes/home/widgets/send_payment/send_payment_dialog.dart';
|
||||
|
||||
class QrActionButton extends StatelessWidget {
|
||||
final BindingLiquidSdk liquidSDK;
|
||||
|
||||
const QrActionButton({super.key, required this.liquidSDK});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 32.0),
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: Colors.white,
|
||||
shape: const StadiumBorder(),
|
||||
onPressed: () => _scanBarcode(context),
|
||||
child: const Icon(
|
||||
Icons.qr_code_2,
|
||||
size: 32,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _scanBarcode(BuildContext context) {
|
||||
debugPrint("Scanning for QR Code");
|
||||
Navigator.of(context).push<String?>(
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return const BarcodeScanner();
|
||||
}),
|
||||
).then((barcode) {
|
||||
if (barcode == null || barcode.isEmpty) return;
|
||||
debugPrint("Scanned string: '$barcode'");
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => SendPaymentDialog(barcodeValue: barcode, liquidSdk: liquidSDK),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
class ReceivePaymentDialog extends StatefulWidget {
|
||||
final BindingLiquidSdk liquidSDK;
|
||||
final Stream<List<Payment>> paymentsStream;
|
||||
|
||||
const ReceivePaymentDialog({super.key, required this.liquidSDK, required this.paymentsStream});
|
||||
|
||||
@override
|
||||
State<ReceivePaymentDialog> createState() => _ReceivePaymentDialogState();
|
||||
}
|
||||
|
||||
class _ReceivePaymentDialogState extends State<ReceivePaymentDialog> {
|
||||
final TextEditingController payerAmountController = TextEditingController();
|
||||
|
||||
int? payerAmountSat;
|
||||
int? feesSat;
|
||||
bool creatingInvoice = false;
|
||||
|
||||
String? invoice;
|
||||
String? invoiceId;
|
||||
|
||||
StreamSubscription<List<Payment>>? streamSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
streamSubscription = widget.paymentsStream.listen((paymentList) {
|
||||
if (invoiceId != null && invoiceId!.isNotEmpty) {
|
||||
if (paymentList.any((e) => e.swapId == invoiceId!)) {
|
||||
debugPrint("Payment Received! Id: $invoiceId");
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
streamSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: creatingInvoice ? null : Text(invoice != null ? "Invoice" : "Receive Payment"),
|
||||
content: creatingInvoice || invoice != null
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (invoice != null) ...[
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: QrImageView(
|
||||
embeddedImage: const AssetImage("assets/icons/app_icon.png"),
|
||||
data: invoice!.toUpperCase(),
|
||||
size: 200.0,
|
||||
),
|
||||
),
|
||||
if (payerAmountSat != null && feesSat != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Text('Payer Amount:'),
|
||||
const Expanded(child: SizedBox(width: 0)),
|
||||
Text('$payerAmountSat sats'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Text('Payer Fees:'),
|
||||
const Expanded(child: SizedBox(width: 0)),
|
||||
Text('$feesSat sats'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Text('Receive Amount:'),
|
||||
const Expanded(child: SizedBox(width: 0)),
|
||||
Text('${payerAmountSat! - feesSat!} sats'),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
if (creatingInvoice) ...[
|
||||
const Text('Creating Invoice...'),
|
||||
const SizedBox(height: 16),
|
||||
const CircularProgressIndicator(color: Colors.blue),
|
||||
]
|
||||
],
|
||||
)
|
||||
: TextField(
|
||||
controller: payerAmountController,
|
||||
decoration: const InputDecoration(label: Text("Enter payer amount in sats")),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
actions: creatingInvoice
|
||||
? []
|
||||
: [
|
||||
TextButton(
|
||||
child: const Text("CANCEL"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
if (invoice == null) ...[
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
setState(() => creatingInvoice = true);
|
||||
int amountSat = int.parse(payerAmountController.text);
|
||||
PrepareReceiveRequest prepareReceiveReq =
|
||||
PrepareReceiveRequest(payerAmountSat: amountSat);
|
||||
PrepareReceiveResponse req =
|
||||
await widget.liquidSDK.prepareReceivePayment(req: prepareReceiveReq);
|
||||
setState(() {
|
||||
payerAmountSat = req.payerAmountSat;
|
||||
feesSat = req.feesSat;
|
||||
});
|
||||
ReceivePaymentResponse resp = await widget.liquidSDK.receivePayment(req: req);
|
||||
debugPrint(
|
||||
"Created Invoice for $payerAmountSat sats with $feesSat sats fees.\nInvoice:${resp.invoice}",
|
||||
);
|
||||
setState(() => invoice = resp.invoice);
|
||||
setState(() => invoiceId = resp.id);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
payerAmountSat = null;
|
||||
feesSat = null;
|
||||
invoice = null;
|
||||
invoiceId = null;
|
||||
});
|
||||
final errMsg = "Error receiving payment: $e";
|
||||
debugPrint(errMsg);
|
||||
if (context.mounted) {
|
||||
final snackBar = SnackBar(content: Text(errMsg));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
} finally {
|
||||
setState(() => creatingInvoice = false);
|
||||
}
|
||||
},
|
||||
child: const Text("OK"),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
|
||||
|
||||
class SendPaymentDialog extends StatefulWidget {
|
||||
final String? barcodeValue;
|
||||
final BindingLiquidSdk liquidSdk;
|
||||
|
||||
const SendPaymentDialog({super.key, required this.liquidSdk, this.barcodeValue});
|
||||
|
||||
@override
|
||||
State<SendPaymentDialog> createState() => _SendPaymentDialogState();
|
||||
}
|
||||
|
||||
class _SendPaymentDialogState extends State<SendPaymentDialog> {
|
||||
final TextEditingController invoiceController = TextEditingController();
|
||||
|
||||
bool paymentInProgress = false;
|
||||
|
||||
PrepareSendResponse? sendPaymentReq;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.barcodeValue != null) {
|
||||
invoiceController.text = widget.barcodeValue!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: paymentInProgress ? null : const Text("Send Payment"),
|
||||
content: paymentInProgress
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('${sendPaymentReq == null ? "Preparing" : "Sending"} payment...'),
|
||||
const SizedBox(height: 16),
|
||||
const CircularProgressIndicator(color: Colors.blue),
|
||||
],
|
||||
),
|
||||
)
|
||||
: sendPaymentReq != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Please confirm that you agree to the payment fee of ${sendPaymentReq!.feesSat} sats.',
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: TextField(
|
||||
decoration: InputDecoration(
|
||||
label: const Text("Enter Invoice"),
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.paste, color: Colors.blue),
|
||||
onPressed: () async {
|
||||
final clipboardData = await Clipboard.getData('text/plain');
|
||||
if (clipboardData != null && clipboardData.text != null) {
|
||||
invoiceController.text = clipboardData.text!;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
controller: invoiceController,
|
||||
),
|
||||
actions: paymentInProgress
|
||||
? []
|
||||
: [
|
||||
TextButton(
|
||||
child: const Text("CANCEL"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
sendPaymentReq == null
|
||||
? TextButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
setState(() => paymentInProgress = true);
|
||||
PrepareSendRequest prepareSendReq =
|
||||
PrepareSendRequest(invoice: invoiceController.text);
|
||||
PrepareSendResponse req =
|
||||
await widget.liquidSdk.prepareSendPayment(req: prepareSendReq);
|
||||
debugPrint("PrepareSendResponse for ${req.invoice}, fees: ${req.feesSat}");
|
||||
setState(() => sendPaymentReq = req);
|
||||
} catch (e) {
|
||||
final errMsg = "Error preparing payment: $e";
|
||||
debugPrint(errMsg);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
final snackBar = SnackBar(content: Text(errMsg));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
} finally {
|
||||
setState(() => paymentInProgress = false);
|
||||
}
|
||||
},
|
||||
child: const Text("OK"),
|
||||
)
|
||||
: TextButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
setState(() => paymentInProgress = true);
|
||||
SendPaymentResponse resp = await widget.liquidSdk.sendPayment(req: sendPaymentReq!);
|
||||
debugPrint("Paid ${resp.txid}");
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e) {
|
||||
final errMsg = "Error sending payment: $e";
|
||||
debugPrint(errMsg);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
final snackBar = SnackBar(content: Text(errMsg));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
} finally {
|
||||
setState(() => paymentInProgress = false);
|
||||
}
|
||||
},
|
||||
child: const Text("CONFIRM"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user