diff --git a/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h b/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h index d10f468..45d1aea 100644 --- a/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h +++ b/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h @@ -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); diff --git a/lib/core/src/bindings.rs b/lib/core/src/bindings.rs index 9c7a8e0..07f8e0a 100644 --- a/lib/core/src/bindings.rs +++ b/lib/core/src/bindings.rs @@ -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) } diff --git a/lib/core/src/frb/bridge.io.rs b/lib/core/src/frb/bridge.io.rs index 51fecfd..23808ad 100644 --- a/lib/core/src/frb/bridge.io.rs +++ b/lib/core/src/frb/bridge.io.rs @@ -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, diff --git a/lib/core/src/frb/bridge.rs b/lib/core/src/frb/bridge.rs index d390fa3..73dc26e 100644 --- a/lib/core/src/frb/bridge.rs +++ b/lib/core/src/frb/bridge.rs @@ -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_HANDLER.wrap_normal::( + 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< diff --git a/packages/dart/lib/src/bindings.dart b/packages/dart/lib/src/bindings.dart index de6c814..3d5a263 100644 --- a/packages/dart/lib/src/bindings.dart +++ b/packages/dart/lib/src/bindings.dart @@ -28,6 +28,9 @@ class BindingLiquidSdk extends RustOpaque { Future backup({dynamic hint}) => RustLib.instance.api.crateBindingsBindingLiquidSdkBackup(that: this, hint: hint); + Future emptyWalletCache({dynamic hint}) => + RustLib.instance.api.crateBindingsBindingLiquidSdkEmptyWalletCache(that: this, hint: hint); + Future getInfo({required GetInfoRequest req, dynamic hint}) => RustLib.instance.api.crateBindingsBindingLiquidSdkGetInfo(that: this, req: req, hint: hint); diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index fffbac9..723a04f 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -53,7 +53,7 @@ class RustLib extends BaseEntrypoint { 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 { abstract class RustLibApi extends BaseApi { Future crateBindingsBindingLiquidSdkBackup({required BindingLiquidSdk that, dynamic hint}); + Future crateBindingsBindingLiquidSdkEmptyWalletCache({required BindingLiquidSdk that, dynamic hint}); + Future crateBindingsBindingLiquidSdkGetInfo( {required BindingLiquidSdk that, required GetInfoRequest req, dynamic hint}); @@ -130,6 +132,31 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["that"], ); + @override + Future 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 crateBindingsBindingLiquidSdkGetInfo( {required BindingLiquidSdk that, required GetInfoRequest req, dynamic hint}) { diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index ef0d650..b2e1c95 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -782,6 +782,22 @@ class RustLibWire implements BaseWire { late final _wire__crate__bindings__BindingLiquidSdk_backup = _wire__crate__bindings__BindingLiquidSdk_backupPtr.asFunction(); + 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>( + '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 wire__crate__bindings__BindingLiquidSdk_get_info( int port_, int that, diff --git a/packages/flutter/example/android/gradle.properties b/packages/flutter/example/android/gradle.properties index 94adc3a..2c8900a 100644 --- a/packages/flutter/example/android/gradle.properties +++ b/packages/flutter/example/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +dev.steenbakker.mobile_scanner.useUnbundled=true \ No newline at end of file diff --git a/packages/flutter/example/android/settings.gradle b/packages/flutter/example/android/settings.gradle index 765b878..42fc1f0 100644 --- a/packages/flutter/example/android/settings.gradle +++ b/packages/flutter/example/android/settings.gradle @@ -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" \ No newline at end of file diff --git a/packages/flutter/example/assets/icons/app_icon.png b/packages/flutter/example/assets/icons/app_icon.png new file mode 100644 index 0000000..10f5ede Binary files /dev/null and b/packages/flutter/example/assets/icons/app_icon.png differ diff --git a/packages/flutter/example/ios/Podfile.lock b/packages/flutter/example/ios/Podfile.lock index 94d7541..03501a8 100644 --- a/packages/flutter/example/ios/Podfile.lock +++ b/packages/flutter/example/ios/Podfile.lock @@ -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 diff --git a/packages/flutter/example/lib/main.dart b/packages/flutter/example/lib/main.dart index ec3dbf7..03ea6bd 100644 --- a/packages/flutter/example/lib/main.dart +++ b/packages/flutter/example/lib/main.dart @@ -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 initializeWallet() async { - assert(mnemonic.isNotEmpty, "Please enter your mnemonic."); +Future 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 createState() => _MyAppState(); + State createState() => _AppState(); } -class _MyAppState extends State { +class _AppState extends State { @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( - 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( - 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( - 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!), ); } } diff --git a/packages/flutter/example/lib/routes/connect/connect_page.dart b/packages/flutter/example/lib/routes/connect/connect_page.dart new file mode 100644 index 0000000..6fc577a --- /dev/null +++ b/packages/flutter/example/lib/routes/connect/connect_page.dart @@ -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 createState() => _ConnectPageState(); +} + +class _ConnectPageState extends State { + 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 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 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); + } +} diff --git a/packages/flutter/example/lib/routes/connect/restore_page.dart b/packages/flutter/example/lib/routes/connect/restore_page.dart new file mode 100644 index 0000000..6abe749 --- /dev/null +++ b/packages/flutter/example/lib/routes/connect/restore_page.dart @@ -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 createState() => _RestorePageState(); +} + +class _RestorePageState extends State { + final _formKey = GlobalKey(); + List textFieldControllers = + List.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, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/flutter/example/lib/routes/home/home_page.dart b/packages/flutter/example/lib/routes/home/home_page.dart new file mode 100644 index 0000000..0b7edb9 --- /dev/null +++ b/packages/flutter/example/lib/routes/home/home_page.dart @@ -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 createState() => _HomePageState(); +} + +class _HomePageState extends State { + Stream 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> 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: [ + 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 _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); + } + } + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/balance.dart b/packages/flutter/example/lib/routes/home/widgets/balance.dart new file mode 100644 index 0000000..f512064 --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/balance.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; + +class Balance extends StatelessWidget { + final Stream walletInfoStream; + + const Balance({super.key, required this.walletInfoStream}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + 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), + ), + ); + }, + ); + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/bottom_app_bar.dart b/packages/flutter/example/lib/routes/home/widgets/bottom_app_bar.dart new file mode 100644 index 0000000..b5b4bb6 --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/bottom_app_bar.dart @@ -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> 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, + ), + ), + ), + ], + ), + ); + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/drawer.dart b/packages/flutter/example/lib/routes/home/widgets/drawer.dart new file mode 100644 index 0000000..4563a21 --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/drawer.dart @@ -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 createState() => _HomePageDrawerState(); +} + +class _HomePageDrawerState extends State { + @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); + } + } + }, + ) + ], + ), + ); + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/mnemonics_dialog.dart b/packages/flutter/example/lib/routes/home/widgets/mnemonics_dialog.dart new file mode 100644 index 0000000..9cb38cf --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/mnemonics_dialog.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class MnemonicsDialog extends StatelessWidget { + final List mnemonics; + + MnemonicsDialog({super.key, required this.mnemonics}); + + final textFieldControllers = List.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(); + }, + ), + ], + ); + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/payment_list/payment_list.dart b/packages/flutter/example/lib/routes/home/widgets/payment_list/payment_list.dart new file mode 100644 index 0000000..4e73a40 --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/payment_list/payment_list.dart @@ -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> paymentsStream; + + const PaymentList({super.key, required this.paymentsStream, required this.onRefresh}); + + @override + Widget build(BuildContext context) { + return StreamBuilder>( + 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]), + ), + ); + }, + ); + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/payment_list/widgets/payment_item.dart b/packages/flutter/example/lib/routes/home/widgets/payment_list/widgets/payment_item.dart new file mode 100644 index 0000000..773b974 --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/payment_list/widgets/payment_item.dart @@ -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"; + } + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/qr_scan/barcode_scanner_simple.dart b/packages/flutter/example/lib/routes/home/widgets/qr_scan/barcode_scanner_simple.dart new file mode 100644 index 0000000..b7757be --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/qr_scan/barcode_scanner_simple.dart @@ -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 createState() => _BarcodeScannerState(); +} + +class _BarcodeScannerState extends State with WidgetsBindingObserver { + bool popped = false; + + final MobileScannerController controller = MobileScannerController( + autoStart: false, + torchEnabled: false, + useNewCameraSelector: true, + ); + + Barcode? _barcode; + StreamSubscription? _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 dispose() async { + WidgetsBinding.instance.removeObserver(this); + unawaited(_subscription?.cancel()); + _subscription = null; + super.dispose(); + await controller.dispose(); + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/qr_scan/scan_overlay.dart b/packages/flutter/example/lib/routes/home/widgets/qr_scan/scan_overlay.dart new file mode 100644 index 0000000..799e9ef --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/qr_scan/scan_overlay.dart @@ -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; + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/qr_scan/scanner_button_widgets.dart b/packages/flutter/example/lib/routes/home/widgets/qr_scan/scanner_button_widgets.dart new file mode 100644 index 0000000..fb65949 --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/qr_scan/scanner_button_widgets.dart @@ -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, + ); + } + }, + ); + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/qr_scan/scanner_error_widget.dart b/packages/flutter/example/lib/routes/home/widgets/qr_scan/scanner_error_widget.dart new file mode 100644 index 0000000..fd16fbc --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/qr_scan/scanner_error_widget.dart @@ -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), + ), + ], + ), + ), + ); + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/qr_scan_action_button.dart b/packages/flutter/example/lib/routes/home/widgets/qr_scan_action_button.dart new file mode 100644 index 0000000..f857cc5 --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/qr_scan_action_button.dart @@ -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( + 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), + ); + }); + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/receive_payment/receive_payment_dialog.dart b/packages/flutter/example/lib/routes/home/widgets/receive_payment/receive_payment_dialog.dart new file mode 100644 index 0000000..e1132e9 --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/receive_payment/receive_payment_dialog.dart @@ -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> paymentsStream; + + const ReceivePaymentDialog({super.key, required this.liquidSDK, required this.paymentsStream}); + + @override + State createState() => _ReceivePaymentDialogState(); +} + +class _ReceivePaymentDialogState extends State { + final TextEditingController payerAmountController = TextEditingController(); + + int? payerAmountSat; + int? feesSat; + bool creatingInvoice = false; + + String? invoice; + String? invoiceId; + + StreamSubscription>? 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"), + ), + ] + ], + ); + } +} diff --git a/packages/flutter/example/lib/routes/home/widgets/send_payment/send_payment_dialog.dart b/packages/flutter/example/lib/routes/home/widgets/send_payment/send_payment_dialog.dart new file mode 100644 index 0000000..8ce21be --- /dev/null +++ b/packages/flutter/example/lib/routes/home/widgets/send_payment/send_payment_dialog.dart @@ -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 createState() => _SendPaymentDialogState(); +} + +class _SendPaymentDialogState extends State { + 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"), + ), + ], + ); + } +} diff --git a/packages/flutter/example/lib/services/credentials_manager.dart b/packages/flutter/example/lib/services/credentials_manager.dart new file mode 100644 index 0000000..6b28d47 --- /dev/null +++ b/packages/flutter/example/lib/services/credentials_manager.dart @@ -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 restoreMnemonic() async { + try { + String mnemonicStr = await keyChain.read(accountMnemonic) ?? ""; + debugPrint("Restored credentials successfully"); + return mnemonicStr; + } catch (err) { + throw Exception(err.toString()); + } + } + + // Helper methods + Future _storeMnemonic(String mnemonic) async { + await keyChain.write(accountMnemonic, mnemonic); + } +} diff --git a/packages/flutter/example/lib/services/keychain.dart b/packages/flutter/example/lib/services/keychain.dart new file mode 100644 index 0000000..67bef68 --- /dev/null +++ b/packages/flutter/example/lib/services/keychain.dart @@ -0,0 +1,21 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class KeyChain { + final FlutterSecureStorage _storage = const FlutterSecureStorage(); + + Future 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(); + } +} diff --git a/packages/flutter/example/linux/flutter/generated_plugin_registrant.cc b/packages/flutter/example/linux/flutter/generated_plugin_registrant.cc index e71a16d..d0e7f79 100644 --- a/packages/flutter/example/linux/flutter/generated_plugin_registrant.cc +++ b/packages/flutter/example/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include 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); } diff --git a/packages/flutter/example/linux/flutter/generated_plugins.cmake b/packages/flutter/example/linux/flutter/generated_plugins.cmake index 2f123ea..8479506 100644 --- a/packages/flutter/example/linux/flutter/generated_plugins.cmake +++ b/packages/flutter/example/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift index e777c67..c44ff01 100644 --- a/packages/flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) } diff --git a/packages/flutter/example/pubspec.lock b/packages/flutter/example/pubspec.lock index 27df155..46c5bf2 100644 --- a/packages/flutter/example/pubspec.lock +++ b/packages/flutter/example/pubspec.lock @@ -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" diff --git a/packages/flutter/example/pubspec.yaml b/packages/flutter/example/pubspec.yaml index 0e60a76..447312e 100644 --- a/packages/flutter/example/pubspec.yaml +++ b/packages/flutter/example/pubspec.yaml @@ -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 \ No newline at end of file + uses-material-design: true + assets: + - assets/icons/ \ No newline at end of file diff --git a/packages/flutter/example/windows/flutter/generated_plugin_registrant.cc b/packages/flutter/example/windows/flutter/generated_plugin_registrant.cc index 8b6d468..0c50753 100644 --- a/packages/flutter/example/windows/flutter/generated_plugin_registrant.cc +++ b/packages/flutter/example/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/packages/flutter/example/windows/flutter/generated_plugins.cmake b/packages/flutter/example/windows/flutter/generated_plugins.cmake index f2ad876..9273adb 100644 --- a/packages/flutter/example/windows/flutter/generated_plugins.cmake +++ b/packages/flutter/example/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index 079df93..4ddb85d 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -54,6 +54,23 @@ class FlutterBreezLiquidBindings { _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_backupPtr .asFunction(); + 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>( + '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 frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_get_info( int port_, int that,