mirror of
https://github.com/aljazceru/breez-sdk-liquid.git
synced 2026-01-05 23:24:25 +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:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
dev.steenbakker.mobile_scanner.useUnbundled=true
|
||||
@@ -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"
|
||||
BIN
packages/flutter/example/assets/icons/app_icon.png
Normal file
BIN
packages/flutter/example/assets/icons/app_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -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
|
||||
|
||||
|
||||
@@ -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!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
21
packages/flutter/example/lib/services/keychain.dart
Normal file
21
packages/flutter/example/lib/services/keychain.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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/
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user