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:
Erdem Yerebasmaz
2024-05-24 12:12:29 +03:00
committed by GitHub
parent c7b46314f4
commit 7b1b78a2d9
38 changed files with 1821 additions and 167 deletions

View File

@@ -139,6 +139,9 @@ typedef struct wire_cst_send_payment_response {
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_backup(int64_t port_,
uintptr_t that);
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache(int64_t port_,
uintptr_t that);
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_get_info(int64_t port_,
uintptr_t that,
struct wire_cst_get_info_request *req);
@@ -210,6 +213,7 @@ static int64_t dummy_method_to_enforce_bundling(void) {
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_backup);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_get_info);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_list_payments);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_prepare_receive_payment);

View File

@@ -56,6 +56,10 @@ impl BindingLiquidSdk {
self.sdk.backup().map_err(Into::into)
}
pub fn empty_wallet_cache(&self) -> Result<(), LiquidSdkError> {
self.sdk.empty_wallet_cache().map_err(Into::into)
}
pub fn restore(&self, req: RestoreRequest) -> Result<(), LiquidSdkError> {
self.sdk.restore(req).map_err(Into::into)
}

View File

@@ -466,6 +466,14 @@ pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_ba
wire__crate__bindings__BindingLiquidSdk_backup_impl(port_, that)
}
#[no_mangle]
pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache(
port_: i64,
that: usize,
) {
wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache_impl(port_, that)
}
#[no_mangle]
pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_get_info(
port_: i64,

View File

@@ -33,7 +33,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueNom,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.0.0-dev.35";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 2052012510;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1284301568;
// Section: executor
@@ -77,6 +77,42 @@ fn wire__crate__bindings__BindingLiquidSdk_backup_impl(
},
)
}
fn wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
that: impl CstDecode<
RustOpaqueNom<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<BindingLiquidSdk>>,
>,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::DcoCodec, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "BindingLiquidSdk_empty_wallet_cache",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let api_that = that.cst_decode();
move |context| {
transform_result_dco((move || {
let mut api_that_decoded = None;
let decode_indices_ =
flutter_rust_bridge::for_generated::rust_auto_opaque_decode_compute_order(
vec![api_that.rust_auto_opaque_lock_order_info(0, false)],
);
for i in decode_indices_ {
match i {
0 => {
api_that_decoded = Some(api_that.rust_auto_opaque_decode_sync_ref())
}
_ => unreachable!(),
}
}
let api_that = api_that_decoded.unwrap();
crate::bindings::BindingLiquidSdk::empty_wallet_cache(&api_that)
})())
}
},
)
}
fn wire__crate__bindings__BindingLiquidSdk_get_info_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
that: impl CstDecode<

View File

@@ -28,6 +28,9 @@ class BindingLiquidSdk extends RustOpaque {
Future<void> backup({dynamic hint}) =>
RustLib.instance.api.crateBindingsBindingLiquidSdkBackup(that: this, hint: hint);
Future<void> emptyWalletCache({dynamic hint}) =>
RustLib.instance.api.crateBindingsBindingLiquidSdkEmptyWalletCache(that: this, hint: hint);
Future<GetInfoResponse> getInfo({required GetInfoRequest req, dynamic hint}) =>
RustLib.instance.api.crateBindingsBindingLiquidSdkGetInfo(that: this, req: req, hint: hint);

View File

@@ -53,7 +53,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.0.0-dev.35';
@override
int get rustContentHash => 2052012510;
int get rustContentHash => 1284301568;
static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
stem: 'breez_liquid_sdk',
@@ -65,6 +65,8 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
abstract class RustLibApi extends BaseApi {
Future<void> crateBindingsBindingLiquidSdkBackup({required BindingLiquidSdk that, dynamic hint});
Future<void> crateBindingsBindingLiquidSdkEmptyWalletCache({required BindingLiquidSdk that, dynamic hint});
Future<GetInfoResponse> crateBindingsBindingLiquidSdkGetInfo(
{required BindingLiquidSdk that, required GetInfoRequest req, dynamic hint});
@@ -130,6 +132,31 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["that"],
);
@override
Future<void> crateBindingsBindingLiquidSdkEmptyWalletCache({required BindingLiquidSdk that, dynamic hint}) {
return handler.executeNormal(NormalTask(
callFfi: (port_) {
var arg0 =
cst_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk(
that);
return wire.wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache(port_, arg0);
},
codec: DcoCodec(
decodeSuccessData: dco_decode_unit,
decodeErrorData: dco_decode_liquid_sdk_error,
),
constMeta: kCrateBindingsBindingLiquidSdkEmptyWalletCacheConstMeta,
argValues: [that],
apiImpl: this,
hint: hint,
));
}
TaskConstMeta get kCrateBindingsBindingLiquidSdkEmptyWalletCacheConstMeta => const TaskConstMeta(
debugName: "BindingLiquidSdk_empty_wallet_cache",
argNames: ["that"],
);
@override
Future<GetInfoResponse> crateBindingsBindingLiquidSdkGetInfo(
{required BindingLiquidSdk that, required GetInfoRequest req, dynamic hint}) {

View File

@@ -782,6 +782,22 @@ class RustLibWire implements BaseWire {
late final _wire__crate__bindings__BindingLiquidSdk_backup =
_wire__crate__bindings__BindingLiquidSdk_backupPtr.asFunction<void Function(int, int)>();
void wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache(
int port_,
int that,
) {
return _wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache(
port_,
that,
);
}
late final _wire__crate__bindings__BindingLiquidSdk_empty_wallet_cachePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.UintPtr)>>(
'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache');
late final _wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache =
_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cachePtr.asFunction<void Function(int, int)>();
void wire__crate__bindings__BindingLiquidSdk_get_info(
int port_,
int that,

View File

@@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
dev.steenbakker.mobile_scanner.useUnbundled=true

View File

@@ -19,7 +19,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.1.2" apply false
id "org.jetbrains.kotlin.android" version "1.6.10" apply false
// mobile_scanner requires 1.7.20
id "org.jetbrains.kotlin.android" version "1.7.20" apply false
}
include ":app"

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -20,8 +20,8 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_breez_liquid: 9467b5fef4bc75e7a51979203a9ae727dd4b05d0
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
flutter_breez_liquid: 90494dd8df26d6258f0d2a90663204ee6257ede2
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048

View File

@@ -1,179 +1,56 @@
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
import 'package:flutter_breez_liquid_example/routes/connect/connect_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:flutter_breez_liquid_example/services/keychain.dart';
import 'package:path_provider/path_provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initialize();
BindingLiquidSdk liquidSDK = await initializeWallet();
runApp(MyApp(liquidSDK));
final credentialsManager = CredentialsManager(keyChain: KeyChain());
final mnemonic = await credentialsManager.restoreMnemonic();
BindingLiquidSdk? liquidSDK;
if (mnemonic.isNotEmpty) {
liquidSDK = await reconnect(mnemonic: mnemonic);
}
runApp(App(credentialsManager: credentialsManager, liquidSDK: liquidSDK));
}
const String mnemonic = "";
Future<BindingLiquidSdk> initializeWallet() async {
assert(mnemonic.isNotEmpty, "Please enter your mnemonic.");
Future<BindingLiquidSdk> reconnect({
required String mnemonic,
Network network = Network.liquid,
}) async {
final dataDir = await getApplicationDocumentsDirectory();
final req = ConnectRequest(
mnemonic: mnemonic,
dataDir: dataDir.path,
network: Network.liquid,
network: network,
);
return await connect(req: req);
}
class MyApp extends StatefulWidget {
final BindingLiquidSdk liquidSDK;
class App extends StatefulWidget {
final CredentialsManager credentialsManager;
final BindingLiquidSdk? liquidSDK;
const App({super.key, required this.credentialsManager, this.liquidSDK});
const MyApp(this.liquidSDK, {super.key});
static const title = 'Breez Liquid SDK Demo';
@override
State<MyApp> createState() => _MyAppState();
State<App> createState() => _AppState();
}
class _MyAppState extends State<MyApp> {
class _AppState extends State<App> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Breez Liquid Native Packages'),
),
body: Padding(
padding: const EdgeInsets.all(10),
child: SingleChildScrollView(
child: Column(
children: [
FutureBuilder<GetInfoResponse>(
future: widget.liquidSDK.getInfo(
req: const GetInfoRequest(
withScan: true,
),
),
initialData: null,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const Text('Loading...');
}
if (snapshot.requireData.balanceSat.isNaN) {
return const Text('No balance.');
}
final walletInfo = snapshot.data!;
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
"Balance",
style: Theme.of(context).textTheme.headlineSmall,
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 32.0),
child: Center(
child: Text(
"${walletInfo.balanceSat} sats",
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
ListTile(
title: Text(
"pubKey: ${walletInfo.pubkey}",
style: Theme.of(context).textTheme.bodySmall,
),
),
],
);
},
),
const SizedBox(height: 16.0),
FutureBuilder<PrepareReceiveResponse>(
future: widget.liquidSDK.prepareReceivePayment(
req: const PrepareReceiveRequest(payerAmountSat: 1000),
),
initialData: null,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const Text('Loading...');
}
final prepareReceiveResponse = snapshot.data!;
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
"Preparing a receive payment of 1000 sats",
style: Theme.of(context).textTheme.headlineSmall,
),
),
ListTile(
title: Text("Payer Amount: ${prepareReceiveResponse.payerAmountSat} (in sats)"),
),
ListTile(
title: Text("Fees: ${prepareReceiveResponse.feesSat} (in sats)"),
),
const SizedBox(height: 16.0),
FutureBuilder<ReceivePaymentResponse>(
future: widget.liquidSDK.receivePayment(req: prepareReceiveResponse),
initialData: null,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const Text('Loading...');
}
if (snapshot.requireData.id.isEmpty) {
return const Text('Missing invoice id');
}
final receivePaymentResponse = snapshot.data!;
debugPrint("Invoice ID: ${receivePaymentResponse.id}");
debugPrint("Invoice: ${receivePaymentResponse.invoice}");
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
"Invoice for receive payment of 1000 sats",
style: Theme.of(context).textTheme.headlineSmall,
),
),
ListTile(
title: Text("Invoice ID: ${receivePaymentResponse.id}"),
),
ListTile(
title: Text("Invoice: ${receivePaymentResponse.invoice}"),
),
],
);
},
),
],
);
},
),
],
),
),
),
),
title: App.title,
theme: ThemeData.from(colorScheme: ColorScheme.fromSeed(seedColor: Colors.white), useMaterial3: true),
home: widget.liquidSDK == null
? ConnectPage(credentialsManager: widget.credentialsManager)
: HomePage(credentialsManager: widget.credentialsManager, liquidSDK: widget.liquidSDK!),
);
}
}

View 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);
}
}

View 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,
),
),
],
),
),
),
);
}
}

View 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);
}
}
}
}

View File

@@ -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),
),
);
},
);
}
}

View File

@@ -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,
),
),
),
],
),
);
}
}

View 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);
}
}
},
)
],
),
);
}
}

View File

@@ -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();
},
),
],
);
}
}

View File

@@ -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]),
),
);
},
);
}
}

View File

@@ -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";
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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,
);
}
},
);
}
}

View File

@@ -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),
),
],
),
),
);
}
}

View File

@@ -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),
);
});
}
}

View File

@@ -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"),
),
]
],
);
}
}

View File

@@ -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"),
),
],
);
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter_breez_liquid_example/services/keychain.dart';
class CredentialsManager {
static const String accountMnemonic = "account_mnemonic";
final KeyChain keyChain;
CredentialsManager({required this.keyChain});
Future storeMnemonic({required String mnemonic}) async {
try {
await _storeMnemonic(mnemonic);
debugPrint("Stored credentials successfully");
} catch (err) {
throw Exception(err.toString());
}
}
Future<String> restoreMnemonic() async {
try {
String mnemonicStr = await keyChain.read(accountMnemonic) ?? "";
debugPrint("Restored credentials successfully");
return mnemonicStr;
} catch (err) {
throw Exception(err.toString());
}
}
// Helper methods
Future<void> _storeMnemonic(String mnemonic) async {
await keyChain.write(accountMnemonic, mnemonic);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class KeyChain {
final FlutterSecureStorage _storage = const FlutterSecureStorage();
Future<String?> read(String key) {
return _storage.read(key: key);
}
Future write(String key, String value) {
return _storage.write(key: key, value: value);
}
Future delete(String key) {
return _storage.delete(key: key);
}
Future clear() {
return _storage.deleteAll();
}
}

View File

@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -5,8 +5,12 @@
import FlutterMacOS
import Foundation
import flutter_secure_storage_macos
import mobile_scanner
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}

View File

@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
bip39:
dependency: "direct main"
description:
name: bip39
sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc
url: "https://pub.dev"
source: hosted
version: "1.0.6"
boolean_selector:
dependency: transitive
description:
@@ -72,6 +80,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.18.0"
convert:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.dev"
source: hosted
version: "3.0.3"
fake_async:
dependency: transitive
description:
@@ -132,11 +156,64 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0-dev.35"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0"
url: "https://pub.dev"
source: hosted
version: "9.2.2"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
freezed_annotation:
dependency: transitive
description:
@@ -153,14 +230,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
hex:
dependency: transitive
description:
name: hex
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.7.1"
version: "0.6.7"
json_annotation:
dependency: transitive
description:
@@ -233,6 +326,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.12.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: b8c0e9afcfd52534f85ec666f3d52156f560b5e6c25b1e3d4fe2087763607926
url: "https://pub.dev"
source: hosted
version: "5.1.1"
package_config:
dependency: transitive
description:
@@ -313,6 +414,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
quiver:
dependency: transitive
description:
@@ -374,6 +499,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.0"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
vector_math:
dependency: transitive
description:
@@ -390,6 +523,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "14.2.1"
web:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
win32:
dependency: transitive
description:
@@ -424,4 +565,4 @@ packages:
version: "2.2.1"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.19.0"

View File

@@ -13,13 +13,18 @@ dependencies:
flutter_rust_bridge: ^2.0.0-dev.35
flutter_breez_liquid: ^0.1.0
# When depending on this package from a real application you should use:
# flutter_breez_liquid: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
# path: ../
bip39: ^1.0.6
flutter_secure_storage: ^9.2.2
mobile_scanner: ^5.1.1
path_provider: ^2.1.3
# When depending on this package from a real application you should use:
# flutter_breez_liquid: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
# path: ../
qr_flutter: ^4.1.0
intl: ^0.19.0
dev_dependencies:
flutter_test:
@@ -29,4 +34,6 @@ dev_dependencies:
flutter:
uses-material-design: true
uses-material-design: true
assets:
- assets/icons/

View File

@@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -54,6 +54,23 @@ class FlutterBreezLiquidBindings {
_frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_backupPtr
.asFunction<void Function(int, int)>();
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache(
int port_,
int that,
) {
return _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache(
port_,
that,
);
}
late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cachePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.UintPtr)>>(
'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache');
late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache =
_frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cachePtr
.asFunction<void Function(int, int)>();
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_get_info(
int port_,
int that,