From 2a9769ba9cfb3d91bc882d4bb834ff32d9e4f012 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Sat, 6 Dec 2025 15:18:01 +0100 Subject: [PATCH 1/9] fix: loading wallet after creation/restoring and abstracting the wallet type --- lib/main.dart | 11 ++++- lib/packages/service/app_store.dart | 9 ++--- .../service/dfx/dfx_auth_service.dart | 2 +- lib/packages/service/dfx/dfx_service.dart | 2 +- lib/packages/wallet/wallet.dart | 25 +++++++++--- lib/packages/wallet/wallet_account.dart | 40 +++++++++---------- .../create_wallet/create_wallet_view.dart | 6 ++- lib/screens/home/bloc/home_bloc.dart | 1 + lib/screens/home/bloc/home_event.dart | 2 +- lib/screens/home/bloc/home_state.dart | 4 +- .../restore_wallet/restore_wallet_view.dart | 4 +- .../settings_seed/settings_seed_page.dart | 8 ++-- 12 files changed, 69 insertions(+), 45 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 6032bb78..3b86faeb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:realunit_wallet/packages/service/app_store.dart'; import 'package:realunit_wallet/packages/service/balance_service.dart'; import 'package:realunit_wallet/packages/utils/fuck_firebase.dart'; import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart'; +import 'package:realunit_wallet/screens/onboarding/onboarding_completed_page.dart'; import 'package:realunit_wallet/screens/settings/bloc/settings_bloc.dart'; import 'package:realunit_wallet/styles/themes.dart'; @@ -113,7 +114,15 @@ class _WalletAppState extends State { builder: (context, child) => BlocListener( listener: (context, state) { if (!state.isLoadingWallet) { - getIt().go(state.onboardingCompleted ? '/dashboard' : '/welcome'); + var targetRoute = '/welcome'; + if (state.openWallet != null) { + targetRoute = OnboardingCompletedPage.route; + if (state.onboardingCompleted) { + targetRoute = '/dashboard'; + } + } + + getIt().go(targetRoute); } }, child: child, diff --git a/lib/packages/service/app_store.dart b/lib/packages/service/app_store.dart index 9af7b363..af72f9b2 100644 --- a/lib/packages/service/app_store.dart +++ b/lib/packages/service/app_store.dart @@ -9,11 +9,11 @@ import 'package:web3dart/web3dart.dart' as web3; class AppStore { List _nodes = []; final httpClient = Client(); - Wallet? _wallet; + AWallet? _wallet; - set wallet(Wallet wallet_) => _wallet = wallet_; + set wallet(AWallet wallet_) => _wallet = wallet_; - Wallet get wallet { + AWallet get wallet { if (_wallet != null) return _wallet!; throw Exception("No Wallet set"); } @@ -22,8 +22,7 @@ class AppStore { _nodes = await nodeRepository.allNodes; } - String get primaryAddress => "0xc38ac0f01d37243aad8b7e496a3e720560d58320"; - // wallet.currentAccount.primaryAddress.address.hexEip55; + String get primaryAddress => wallet.currentAccount.primaryAddress.address.hexEip55; web3.Web3Client getClient(int chainId) { final node = _nodes.firstWhere( diff --git a/lib/packages/service/dfx/dfx_auth_service.dart b/lib/packages/service/dfx/dfx_auth_service.dart index 496c5555..d3b51c48 100644 --- a/lib/packages/service/dfx/dfx_auth_service.dart +++ b/lib/packages/service/dfx/dfx_auth_service.dart @@ -14,7 +14,7 @@ abstract class DFXAuthService { String get baseUrl => 'api.dfx.swiss'; - WalletAccount get wallet; + AWalletAccount get wallet; String get walletAddress; diff --git a/lib/packages/service/dfx/dfx_service.dart b/lib/packages/service/dfx/dfx_service.dart index 72693b5b..13adbfba 100644 --- a/lib/packages/service/dfx/dfx_service.dart +++ b/lib/packages/service/dfx/dfx_service.dart @@ -24,7 +24,7 @@ class DFXService extends DFXAuthService { String get title => 'DFX.swiss'; @override - WalletAccount get wallet => appStore.wallet.currentAccount; + AWalletAccount get wallet => appStore.wallet.currentAccount; @override String get walletAddress => wallet.primaryAddress.address.hexEip55; diff --git a/lib/packages/wallet/wallet.dart b/lib/packages/wallet/wallet.dart index f9c974cb..e998dc92 100644 --- a/lib/packages/wallet/wallet.dart +++ b/lib/packages/wallet/wallet.dart @@ -2,26 +2,41 @@ import 'package:bip32/bip32.dart'; import 'package:bip39/bip39.dart'; import 'package:realunit_wallet/packages/wallet/wallet_account.dart'; -class Wallet { +enum WalletType { software, bitbox } + +abstract class AWallet { + WalletType get walletType; final int id; - final String seed; String name; /// The Primary account is the account derived from the account index 0 + AWalletAccount get primaryAccount; + AWalletAccount get currentAccount; + + AWallet(this.id, this.name); +} + +class Wallet extends AWallet { + @override + WalletType get walletType => WalletType.software; + + final String seed; + + @override late final WalletAccount primaryAccount; late final BIP32 _bip32; late WalletAccount _currentAccount; + @override WalletAccount get currentAccount => _currentAccount; - Wallet(this.id, this.name, this.seed) { + Wallet(super.id, super.name, this.seed) { final seedBytes = mnemonicToSeed(seed); _bip32 = BIP32.fromSeed(seedBytes); primaryAccount = WalletAccount(_bip32, 0); _currentAccount = primaryAccount; } - void selectAccount(int index) => - _currentAccount = WalletAccount(_bip32, index); + void selectAccount(int index) => _currentAccount = WalletAccount(_bip32, index); } diff --git a/lib/packages/wallet/wallet_account.dart b/lib/packages/wallet/wallet_account.dart index 39ea5ef3..75ab41cb 100644 --- a/lib/packages/wallet/wallet_account.dart +++ b/lib/packages/wallet/wallet_account.dart @@ -4,37 +4,33 @@ import 'package:bip32/bip32.dart'; import 'package:convert/convert.dart'; import 'package:web3dart/web3dart.dart'; -class WalletAccount { - late final CredentialsWithKnownAddress primaryAddress; - final List subAddresses = []; - +abstract class AWalletAccount { final int accountIndex; - final BIP32 root; - int _lastIndex = 0; + final CredentialsWithKnownAddress primaryAddress; - WalletAccount(this.root, this.accountIndex) { - primaryAddress = _getPrivateKeyAt(0); - subAddresses.add(primaryAddress); - nextAddresses(5); - } + AWalletAccount(this.accountIndex, this.primaryAddress); String getDerivationPath(int addressIndex) => "m/44'/60'/$accountIndex'/0/$addressIndex"; - void nextAddresses(int limit) { - final indexes = List.generate(limit, (i) => _lastIndex + i); - for (final index in indexes) { - subAddresses.add(_getPrivateKeyAt(index)); - } - _lastIndex += limit; - } + Future signMessage(String message, {int addressIndex = 0}); +} + +class WalletAccount extends AWalletAccount { + final BIP32 root; + + WalletAccount(this.root, int accountIndex) + : super(accountIndex, _getPrivateKeyAt(root, accountIndex, 0)); - EthPrivateKey _getPrivateKeyAt(int addressIndex) { - final addressAtIndex = root.derivePath(getDerivationPath(addressIndex)); + static EthPrivateKey _getPrivateKeyAt( + BIP32 root, int accountIndex, int addressIndex) { + final addressAtIndex = + root.derivePath("m/44'/60'/$accountIndex'/0/$addressIndex"); return EthPrivateKey.fromHex(hex.encode(addressAtIndex.privateKey!)); } - String signMessage(String message, {int addressIndex = 0}) => - "0x${hex.encode(_getPrivateKeyAt(addressIndex).signPersonalMessageToUint8List(ascii.encode(message)))}"; + @override + Future signMessage(String message, {int addressIndex = 0}) async => + "0x${hex.encode(_getPrivateKeyAt(root, addressIndex, addressIndex).signPersonalMessageToUint8List(ascii.encode(message)))}"; } diff --git a/lib/screens/create_wallet/create_wallet_view.dart b/lib/screens/create_wallet/create_wallet_view.dart index ca4301db..426eaebf 100644 --- a/lib/screens/create_wallet/create_wallet_view.dart +++ b/lib/screens/create_wallet/create_wallet_view.dart @@ -6,7 +6,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:realunit_wallet/generated/i18n.dart'; import 'package:realunit_wallet/screens/create_wallet/bloc/create_wallet_cubit.dart'; -import 'package:realunit_wallet/screens/onboarding/onboarding_completed_page.dart'; +import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart'; import 'package:realunit_wallet/styles/colors.dart'; import 'package:realunit_wallet/styles/icons.dart'; import 'package:realunit_wallet/styles/styles.dart'; @@ -120,7 +120,9 @@ class CreateWalletView extends StatelessWidget { child: SizedBox( width: double.infinity, child: TextButton( - onPressed: () => context.push(OnboardingCompletedPage.route), + onPressed: () => context + .read() + .add(LoadWalletEvent(state.wallet!)), style: kFullwidthBlueButtonStyle, child: Text( S.of(context).create_wallet_confirm, diff --git a/lib/screens/home/bloc/home_bloc.dart b/lib/screens/home/bloc/home_bloc.dart index 21a5f5cc..01ea9b3e 100644 --- a/lib/screens/home/bloc/home_bloc.dart +++ b/lib/screens/home/bloc/home_bloc.dart @@ -92,6 +92,7 @@ class HomeBloc extends Bloc { } Future setupFiatService(Emitter emit) async { + if (_appStore.wallet.walletType != WalletType.software) return; try { await _dfxService.getAuthToken(); emit(state.copyWith(isFiatServiceAvailable: _dfxService.isAvailable)); diff --git a/lib/screens/home/bloc/home_event.dart b/lib/screens/home/bloc/home_event.dart index ff737aa7..d7e915f7 100644 --- a/lib/screens/home/bloc/home_event.dart +++ b/lib/screens/home/bloc/home_event.dart @@ -18,7 +18,7 @@ final class DeleteCurrentWalletEvent extends HomeEvent { final class LoadWalletEvent extends HomeEvent { const LoadWalletEvent(this.wallet); - final Wallet wallet; + final AWallet wallet; @override List get props => [wallet]; diff --git a/lib/screens/home/bloc/home_state.dart b/lib/screens/home/bloc/home_state.dart index 2ac8623a..cc4e134b 100644 --- a/lib/screens/home/bloc/home_state.dart +++ b/lib/screens/home/bloc/home_state.dart @@ -8,13 +8,13 @@ final class HomeState { this.onboardingCompleted = false, }); - final Wallet? openWallet; + final AWallet? openWallet; final bool isLoadingWallet; final bool isFiatServiceAvailable; final bool onboardingCompleted; HomeState copyWith({ - Wallet? openWallet, + AWallet? openWallet, bool? isLoadingWallet, bool? isFiatServiceAvailable, bool? onboardingCompleted, diff --git a/lib/screens/restore_wallet/restore_wallet_view.dart b/lib/screens/restore_wallet/restore_wallet_view.dart index d013e291..6a1af6ae 100644 --- a/lib/screens/restore_wallet/restore_wallet_view.dart +++ b/lib/screens/restore_wallet/restore_wallet_view.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:realunit_wallet/generated/i18n.dart'; -import 'package:realunit_wallet/screens/onboarding/onboarding_completed_page.dart'; +import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart'; import 'package:realunit_wallet/screens/restore_wallet/bloc/restore_wallet_cubit.dart'; import 'package:realunit_wallet/screens/restore_wallet/widgets/seed_editing_controller.dart'; import 'package:realunit_wallet/styles/colors.dart'; @@ -19,7 +19,7 @@ class RestoreWalletView extends StatelessWidget { Widget build(BuildContext context) => BlocListener( listener: (context, state) { if (state.wallet != null) { - context.push(OnboardingCompletedPage.route); + context.read().add(LoadWalletEvent(state.wallet!)); } }, child: Scaffold( diff --git a/lib/screens/settings_seed/settings_seed_page.dart b/lib/screens/settings_seed/settings_seed_page.dart index a1ab8003..919c3508 100644 --- a/lib/screens/settings_seed/settings_seed_page.dart +++ b/lib/screens/settings_seed/settings_seed_page.dart @@ -1,16 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/di.dart'; import 'package:realunit_wallet/packages/service/app_store.dart'; +import 'package:realunit_wallet/packages/wallet/wallet.dart'; import 'package:realunit_wallet/screens/settings_seed/bloc/settings_seed_cubit.dart'; import 'package:realunit_wallet/screens/settings_seed/settings_seed_view.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsSeedPage extends StatelessWidget { const SettingsSeedPage({super.key}); @override Widget build(BuildContext context) => BlocProvider( - create: (_) => SettingsSeedCubit(getIt().wallet.seed), + create: (_) => + SettingsSeedCubit((getIt().wallet as Wallet).seed), child: SettingsSeedView(), ); } From 391311e17ca27464e5105ae093af6629f538a203 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Sun, 7 Dec 2025 02:33:46 +0100 Subject: [PATCH 2/9] feat: add bitbox --- .../images/illustrations/bitbox_connect.svg | 44 +++++ .../images/illustrations/bitbox_connected.svg | 42 ++++ assets/languages/strings_de.arb | 6 + assets/languages/strings_en.arb | 6 + lib/di.dart | 28 +-- lib/main.dart | 3 +- lib/packages/hardware_wallet/bitbox.dart | 26 +++ .../hardware_wallet/bitbox_credentials.dart | 78 ++++++++ .../repository/wallet_repository.dart | 11 +- lib/packages/service/app_store.dart | 2 +- lib/packages/service/wallet_service.dart | 26 ++- lib/packages/storage/database.dart | 6 +- lib/packages/storage/wallet_storage.dart | 12 +- lib/packages/wallet/wallet.dart | 22 +++ lib/packages/wallet/wallet_account.dart | 8 + .../dashboard/widgets/section_balance.dart | 179 ++++++++---------- .../bloc/connect_bitbox_cubit.dart | 67 +++++++ .../connect_bitbox_page.dart | 17 ++ .../connect_bitbox_view.dart | 71 +++++++ .../widgets/connect_content.dart | 39 ++++ lib/screens/settings/settings_page.dart | 15 +- lib/screens/welcome/welcome_page.dart | 25 ++- lib/styles/colors.dart | 1 + lib/styles/styles.dart | 25 ++- lib/widgets/handlebars.dart | 7 +- lib/widgets/secondary_button.dart | 24 +++ pubspec.lock | 20 +- pubspec.yaml | 10 +- 28 files changed, 663 insertions(+), 157 deletions(-) create mode 100644 assets/images/illustrations/bitbox_connect.svg create mode 100644 assets/images/illustrations/bitbox_connected.svg create mode 100644 lib/packages/hardware_wallet/bitbox.dart create mode 100644 lib/packages/hardware_wallet/bitbox_credentials.dart create mode 100644 lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart create mode 100644 lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart create mode 100644 lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart create mode 100644 lib/screens/hardware_connect_bitbox/widgets/connect_content.dart create mode 100644 lib/widgets/secondary_button.dart diff --git a/assets/images/illustrations/bitbox_connect.svg b/assets/images/illustrations/bitbox_connect.svg new file mode 100644 index 00000000..18842b9d --- /dev/null +++ b/assets/images/illustrations/bitbox_connect.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/illustrations/bitbox_connected.svg b/assets/images/illustrations/bitbox_connected.svg new file mode 100644 index 00000000..427d1d92 --- /dev/null +++ b/assets/images/illustrations/bitbox_connected.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/languages/strings_de.arb b/assets/languages/strings_de.arb index 8f790658..14ee279d 100644 --- a/assets/languages/strings_de.arb +++ b/assets/languages/strings_de.arb @@ -3,9 +3,15 @@ "balance": "Guthaben", "bitbox": "BitBox", "blockchain": "Blockchain", + "cancel": "Abbrechen", "collect_interest": "Auszahlen", "confirm": "Ausführen", + "connect_bitbox_content": "Bitte verbinden Sie Ihre BitBox02 mit Ihrem Smartphone.", + "connect_bitbox_content_ios": "Bitte verbinden Sie Ihre BitBox02 mit Ihrem Smartphone und aktivieren Sie zusätzlich Bluetooth.", + "connect_bitbox_title": "BitBox02 verbinden", "connect_with": "Mit ${wallet} verbinden", + "connected_bitbox_content": "Bitte folgen Sie nun den Anweisungen auf Ihrer BitBox02.", + "connected_bitbox_title": "Verbindung erfolgreich", "copy_seed": "Seed kopieren", "create_wallet": "Neue Wallet erstellen", "create_wallet_confirm": "Ich habe es gesichert", diff --git a/assets/languages/strings_en.arb b/assets/languages/strings_en.arb index 5f3f27b1..3a58b0d3 100644 --- a/assets/languages/strings_en.arb +++ b/assets/languages/strings_en.arb @@ -3,9 +3,15 @@ "balance": "Balance", "bitbox": "BitBox", "blockchain": "Blockchain", + "cancel": "Cancel", "collect_interest": "Collect", "confirm": "Confirm", + "connect_bitbox_content": "Please connect your BitBox02 with your Smartphone.", + "connect_bitbox_content_ios": "Please connect your BitBox02 with your Smartphone and activate Bluetooth.", + "connect_bitbox_title": "Connect BitBox02", "connect_with": "Connect with ${wallet}", + "connected_bitbox_content": "Please follow the steps on your BitBox02.", + "connected_bitbox_title": "Connection successful", "copy_seed": "Copy seed", "create_wallet": "Create new Wallet", "create_wallet_confirm": "I’ve written it down", diff --git a/lib/di.dart b/lib/di.dart index 018610ad..a09b9ba8 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:get_it/get_it.dart'; +import 'package:realunit_wallet/packages/hardware_wallet/bitbox.dart'; import 'package:realunit_wallet/packages/open_crypto_pay/open_crypto_pay_service.dart'; import 'package:realunit_wallet/packages/repository/asset_repository.dart'; import 'package:realunit_wallet/packages/repository/balance_repository.dart'; @@ -74,25 +75,29 @@ void setupRepositories() { getIt.registerFactory(() => BalanceRepository(getIt())); getIt.registerFactory(() => AssetRepository(getIt())); getIt.registerFactory(() => NodeRepository(getIt())); - getIt - .registerFactory(() => TransactionRepository(getIt(), getIt())); + getIt.registerFactory(() => + TransactionRepository(getIt(), getIt())); } void setupServices() { - getIt - .registerFactory(() => WalletService(getIt(), getIt())); + getIt.registerSingleton(BalanceService( + getIt(), getIt(), getIt())); - getIt.registerSingleton( - BalanceService(getIt(), getIt(), getIt())); + getIt.registerSingleton(BitboxService()); + getIt.registerFactory(() => WalletService( + getIt(), + getIt(), + getIt(), + )); - getIt.registerFactory(() => TransactionHistoryService( - getIt(), getIt(), getIt())); + getIt.registerFactory(() => TransactionHistoryService(getIt(), + getIt(), getIt())); getIt.registerFactory(() => OpenCryptoPayService()); getIt.registerFactory(() => DFXPriceService(getIt())); getIt.registerFactory(() => SettingsService(getIt())); - getIt.registerFactory( - () => DFXService(getIt(), getIt(), getIt())); + getIt.registerFactory(() => DFXService(getIt(), + getIt(), getIt())); } void setupBlocs() { @@ -108,4 +113,5 @@ void setupBlocs() { getIt.registerFactory(() => RestoreWalletCubit(getIt())); } -Future _existsDatabaseFile() async => File(await AppDatabase.getDatabasePath()).exists(); +Future _existsDatabaseFile() async => + File(await AppDatabase.getDatabasePath()).exists(); diff --git a/lib/main.dart b/lib/main.dart index 3b86faeb..58c0aa7f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -81,7 +81,8 @@ class _WalletAppState extends State { void _onDetached() => developer.log('detached'); void _onResumed() { - getIt().updateERC20Balances(getIt().primaryAddress); + getIt() + .updateERC20Balances(getIt().primaryAddress); } void _onInactive() => developer.log('inactive', name: 'AppLifecycleListener'); diff --git a/lib/packages/hardware_wallet/bitbox.dart b/lib/packages/hardware_wallet/bitbox.dart new file mode 100644 index 00000000..a16ed382 --- /dev/null +++ b/lib/packages/hardware_wallet/bitbox.dart @@ -0,0 +1,26 @@ +import 'package:bitbox_flutter/bitbox_flutter.dart' as sdk; +import 'package:realunit_wallet/packages/hardware_wallet/bitbox_credentials.dart'; + +class BitboxService { + BitboxService() { + bitboxManager = sdk.BitboxManager(); + } + + late sdk.BitboxManager bitboxManager; + + Future> getAllUsbDevices() => bitboxManager.devices; + + BitboxCredentials getCredentials(String address) => + BitboxCredentials(address)..setBitbox(bitboxManager); + + Future connectDevice(sdk.BitboxDevice device) async { + await bitboxManager.connect(device); + final didInit = await bitboxManager.initBitBox(); + + if (!didInit) throw Exception("Failed to init"); + + final didVerify = await bitboxManager.channelHashVerify(); + + if (!didVerify) throw Exception("Failed to verify"); + } +} diff --git a/lib/packages/hardware_wallet/bitbox_credentials.dart b/lib/packages/hardware_wallet/bitbox_credentials.dart new file mode 100644 index 00000000..3426cf94 --- /dev/null +++ b/lib/packages/hardware_wallet/bitbox_credentials.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:bitbox_flutter/bitbox_manager.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:web3dart/web3dart.dart'; + +class BitboxCredentials extends CredentialsWithKnownAddress { + final String _address; + + BitboxManager? bitboxManager; + String? derivationPath; + + BitboxCredentials(this._address); + + @override + EthereumAddress get address => EthereumAddress.fromHex(_address); + + void setBitbox(BitboxManager connection, [String? derivationPath_]) { + bitboxManager = connection; + derivationPath = derivationPath_ ?? "m/44'/60'/0'/0/0"; + } + + @override + MsgSignature signToEcSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) => + throw UnimplementedError("EvmLedgerCredentials.signToEcSignature"); + + @override + Future signToSignature(Uint8List payload, + {int? chainId, bool isEIP1559 = false}) async { + if (bitboxManager == null) { + throw Exception("Bitbox not connected"); + } + + if (isEIP1559) payload = payload.sublist(1); + final sig = await bitboxManager! + .signETHRLPTransaction(chainId ?? 1, derivationPath!, bytesToHex(payload), isEIP1559); + + final r = bytesToHex(sig.sublist(0, 32)); + final s = bytesToHex(sig.sublist(32, 32 + 32)); + final v = sig.last.toInt(); + + if (isEIP1559) { + return MsgSignature(BigInt.parse(r, radix: 16), BigInt.parse(s, radix: 16), v); + } + + var truncChainId = chainId ?? 1; + while (truncChainId.bitLength > 32) { + truncChainId >>= 8; + } + + final truncTarget = truncChainId * 2 + 35; + + int parity = v; + if (truncTarget & 0xff == v) { + parity = 0; + } else if ((truncTarget + 1) & 0xff == v) { + parity = 1; + } + + // https://github.com/ethereumjs/ethereumjs-util/blob/8ffe697fafb33cefc7b7ec01c11e3a7da787fe0e/src/signature.ts#L26 + final chainIdV = chainId != null ? (parity + (chainId * 2 + 35)) : parity; + + return MsgSignature(BigInt.parse(r, radix: 16), BigInt.parse(s, radix: 16), chainIdV); + } + + @override + Future signPersonalMessage(Uint8List payload, {int? chainId}) async { + if (isNotConnected) throw Exception("Bitbox not connected"); + return await bitboxManager!.signETHMessage(chainId ?? 1, derivationPath!, payload); + } + + @override + Uint8List signPersonalMessageToUint8List(Uint8List payload, {int? chainId}) => + throw UnimplementedError("EvmLedgerCredentials.signPersonalMessageToUint8List"); + + bool get isNotConnected => bitboxManager == null; +} diff --git a/lib/packages/repository/wallet_repository.dart b/lib/packages/repository/wallet_repository.dart index 4f2e31bf..4748d25d 100644 --- a/lib/packages/repository/wallet_repository.dart +++ b/lib/packages/repository/wallet_repository.dart @@ -1,16 +1,19 @@ import 'package:realunit_wallet/packages/storage/database.dart'; import 'package:realunit_wallet/packages/storage/wallet_storage.dart'; +import 'package:realunit_wallet/packages/wallet/wallet.dart'; class WalletRepository { final AppDatabase _appDatabase; const WalletRepository(this._appDatabase); - Future createWallet(String name, String seed) => - _appDatabase.insertWallet(name, seed); + Future createWallet(String name, WalletType type, String seed) => + _appDatabase.insertWallet(name, seed, "", type.index); - Future getWalletById(int id) => - _appDatabase.getWalletById(id); + Future createViewWallet(String name, WalletType type, String address) => + _appDatabase.insertWallet(name, "", address, type.index); + + Future getWalletById(int id) => _appDatabase.getWalletById(id); Future deleteWallet(int id) => _appDatabase.deleteWallet(id); } diff --git a/lib/packages/service/app_store.dart b/lib/packages/service/app_store.dart index af72f9b2..99d5a5c2 100644 --- a/lib/packages/service/app_store.dart +++ b/lib/packages/service/app_store.dart @@ -22,7 +22,7 @@ class AppStore { _nodes = await nodeRepository.allNodes; } - String get primaryAddress => wallet.currentAccount.primaryAddress.address.hexEip55; + String get primaryAddress => wallet.currentAccount.primaryAddress.address.hex; web3.Web3Client getClient(int chainId) { final node = _nodes.firstWhere( diff --git a/lib/packages/service/wallet_service.dart b/lib/packages/service/wallet_service.dart index fcd39230..e9e5ca95 100644 --- a/lib/packages/service/wallet_service.dart +++ b/lib/packages/service/wallet_service.dart @@ -1,4 +1,5 @@ import 'package:bip39/bip39.dart' as bip39; +import 'package:realunit_wallet/packages/hardware_wallet/bitbox.dart'; import 'package:realunit_wallet/packages/repository/settings_repository.dart'; import 'package:realunit_wallet/packages/repository/wallet_repository.dart'; import 'package:realunit_wallet/packages/wallet/wallet.dart'; @@ -6,26 +7,41 @@ import 'package:realunit_wallet/packages/wallet/wallet.dart'; class WalletService{ final WalletRepository _repository; final SettingsRepository _settingsRepository; + final BitboxService _bitboxService; - const WalletService(this._repository, this._settingsRepository); + const WalletService(this._bitboxService, this._repository, this._settingsRepository); Future createWallet(String name) { final mnemonic = bip39.generateMnemonic(); return restoreWallet(name, mnemonic); } + Future createBitboxWallet(String name) async { + final address = await _bitboxService.bitboxManager.getETHAddress(1, "m/44'/60'/0'/0/0"); + final walletId = await _repository.createViewWallet(name, WalletType.bitbox, address); + await _settingsRepository.saveCurrentWalletId(walletId); + return BitboxWallet(walletId, name, address, _bitboxService); + } + Future restoreWallet(String name, String seed) async { - final walletId = await _repository.createWallet(name, seed); + final walletId = await _repository.createWallet(name, WalletType.software, seed); await _settingsRepository.saveCurrentWalletId(walletId); return Wallet(walletId, name, seed); } - Future getWalletById(int id) async { + Future getWalletById(int id) async { final result = (await _repository.getWalletById(id))!; - return Wallet(result.id, result.name, result.seed); + final walletType = WalletType.values[result.type]; + switch (walletType) { + case WalletType.software: + return Wallet(result.id, result.name, result.seed); + case WalletType.bitbox: + return BitboxWallet( + result.id, result.name, result.address, _bitboxService); + } } - Future getCurrentWallet() async { + Future getCurrentWallet() async { final id = _settingsRepository.currentWalletId!; return getWalletById(id); } diff --git a/lib/packages/storage/database.dart b/lib/packages/storage/database.dart index dca63ad1..e60cba14 100644 --- a/lib/packages/storage/database.dart +++ b/lib/packages/storage/database.dart @@ -52,7 +52,7 @@ class AppDatabase extends _$AppDatabase { : super(_openDatabase(encryptionPassword)); @override - int get schemaVersion => 2; + int get schemaVersion => 1; @override MigrationStrategy get migration => MigrationStrategy( @@ -60,9 +60,7 @@ class AppDatabase extends _$AppDatabase { await m.createAll(); }, onUpgrade: (Migrator m, int from, int to) async { - if (from < 2) { - await m.createTable(keyValueCache); - } + }, ); diff --git a/lib/packages/storage/wallet_storage.dart b/lib/packages/storage/wallet_storage.dart index 974143bc..5f52b4fb 100644 --- a/lib/packages/storage/wallet_storage.dart +++ b/lib/packages/storage/wallet_storage.dart @@ -1,9 +1,11 @@ -import 'package:realunit_wallet/packages/storage/database.dart'; import 'package:drift/drift.dart'; +import 'package:realunit_wallet/packages/storage/database.dart'; extension WalletStorage on AppDatabase { - Future insertWallet(String name, String seed) => into(walletInfos) - .insert(WalletInfosCompanion.insert(name: name, seed: seed)); + Future insertWallet( + String name, String seed, String address, int walletType) => + into(walletInfos).insert(WalletInfosCompanion.insert( + name: name, seed: seed, address: address, type: walletType)); Future getWalletById(int id) => (select(walletInfos)..where((row) => row.id.equals(id))) @@ -33,6 +35,10 @@ class WalletInfos extends Table { TextColumn get name => text()(); TextColumn get seed => text()(); + + TextColumn get address => text()(); + + IntColumn get type => integer()(); } @DataClassName("WalletAccountInfo") diff --git a/lib/packages/wallet/wallet.dart b/lib/packages/wallet/wallet.dart index e998dc92..6077a349 100644 --- a/lib/packages/wallet/wallet.dart +++ b/lib/packages/wallet/wallet.dart @@ -1,5 +1,6 @@ import 'package:bip32/bip32.dart'; import 'package:bip39/bip39.dart'; +import 'package:realunit_wallet/packages/hardware_wallet/bitbox.dart'; import 'package:realunit_wallet/packages/wallet/wallet_account.dart'; enum WalletType { software, bitbox } @@ -40,3 +41,24 @@ class Wallet extends AWallet { void selectAccount(int index) => _currentAccount = WalletAccount(_bip32, index); } + +class BitboxWallet extends AWallet { + @override + WalletType get walletType => WalletType.bitbox; + + final BitboxService _bitboxService; + + @override + late final BitboxWalletAccount primaryAccount; + + late BitboxWalletAccount _currentAccount; + + @override + BitboxWalletAccount get currentAccount => _currentAccount; + + BitboxWallet(super.id, super.name, String address, this._bitboxService) { + primaryAccount = BitboxWalletAccount(0, _bitboxService.getCredentials( + address)); + _currentAccount = primaryAccount; + } +} diff --git a/lib/packages/wallet/wallet_account.dart b/lib/packages/wallet/wallet_account.dart index 75ab41cb..ff12dce5 100644 --- a/lib/packages/wallet/wallet_account.dart +++ b/lib/packages/wallet/wallet_account.dart @@ -34,3 +34,11 @@ class WalletAccount extends AWalletAccount { Future signMessage(String message, {int addressIndex = 0}) async => "0x${hex.encode(_getPrivateKeyAt(root, addressIndex, addressIndex).signPersonalMessageToUint8List(ascii.encode(message)))}"; } + +class BitboxWalletAccount extends AWalletAccount { + BitboxWalletAccount(super.accountIndex, super.primaryAddress); + + @override + Future signMessage(String message, {int addressIndex = 0}) async => + "0x${hex.encode(await primaryAddress.signPersonalMessage(ascii.encode(message)))}"; +} diff --git a/lib/screens/dashboard/widgets/section_balance.dart b/lib/screens/dashboard/widgets/section_balance.dart index f26d3144..7fd5a9d5 100644 --- a/lib/screens/dashboard/widgets/section_balance.dart +++ b/lib/screens/dashboard/widgets/section_balance.dart @@ -1,24 +1,21 @@ import 'dart:developer' as developer; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:realunit_wallet/di.dart'; import 'package:realunit_wallet/generated/i18n.dart'; import 'package:realunit_wallet/packages/open_crypto_pay/exceptions.dart'; import 'package:realunit_wallet/packages/open_crypto_pay/open_crypto_pay_service.dart'; import 'package:realunit_wallet/packages/service/dfx/dfx_service.dart'; import 'package:realunit_wallet/packages/wallet/payment_uri.dart'; -import 'package:realunit_wallet/screens/receive/receive_page.dart'; import 'package:realunit_wallet/screens/send/send_page.dart'; import 'package:realunit_wallet/screens/settings/bloc/settings_bloc.dart'; import 'package:realunit_wallet/styles/colors.dart'; -import 'package:realunit_wallet/styles/styles.dart'; +import 'package:realunit_wallet/styles/icons.dart'; import 'package:realunit_wallet/widgets/action_button.dart'; import 'package:realunit_wallet/widgets/hide_amount_text.dart'; import 'package:realunit_wallet/widgets/qr_scanner.dart'; -import 'package:realunit_wallet/widgets/vertical_icon_button.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; class SectionBalance extends StatelessWidget { final BigInt balance; @@ -37,110 +34,90 @@ class SectionBalance extends StatelessWidget { }); @override - Widget build(BuildContext context) => Container( - width: double.infinity, - color: RealUnitColors.realUnitBlue, - child: SafeArea( - child: Column( - children: [ - Padding( - padding: EdgeInsets.only(left: 16, right: 16, top: 10), - child: Row( - children: [ - if (isFiatServiceAvailable) ...[ - Padding( - padding: EdgeInsets.only(right: 10), - child: ActionButton( - icon: Icons.credit_card, - label: S.of(context).deposit, - onPressed: () => - getIt().launchProvider(context, true), - buttonStyle: kBalanceBarActionButtonStyle, - ), - ), - ActionButton( - icon: Icons.account_balance, - label: S.of(context).withdraw, - onPressed: () => - getIt().launchProvider(context, false), - buttonStyle: kBalanceBarActionButtonStyle, - ), - ], - ], - ), - ), - Padding( - padding: EdgeInsets.only(top: 12, bottom: 12), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - S.of(context).balance, - style: TextStyle( - fontSize: 14, - color: Colors.white.withAlpha(153), + Widget build(BuildContext context) => Column(children: [ + Container( + width: double.infinity, + color: RealUnitColors.realUnitBlue, + child: SafeArea( + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 12, bottom: 12), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).balance, + style: TextStyle( + fontSize: 14, + color: Colors.white.withAlpha(153), + ), ), - ), - InkWell( - onTap: onHideAmountPress, - enableFeedback: false, - child: Padding( - padding: EdgeInsets.only(left: 5), - child: BlocBuilder( - builder: (context, state) => Icon( - state.hideAmounts - ? Icons.visibility_off - : Icons.visibility, - size: 14, - color: Colors.white.withAlpha(153), + InkWell( + onTap: onHideAmountPress, + enableFeedback: false, + child: Padding( + padding: EdgeInsets.only(left: 5), + child: BlocBuilder( + builder: (context, state) => Icon( + state.hideAmounts + ? Icons.visibility_off + : Icons.visibility, + size: 14, + color: Colors.white.withAlpha(153), + ), ), ), ), - ), - ], - ), - HideAmountText( - amount: balance, - style: const TextStyle( - fontSize: 35, - color: Colors.white, - fontFamily: "Satoshi Bold"), - textAlign: TextAlign.center, - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - VerticalIconButton( - onPressed: () => showCupertinoSheet( - context: context, pageBuilder: (_) => ReceivePage()), - icon: const Icon(Icons.arrow_downward, color: Colors.white), - label: S.of(context).receive, + ], + ), + HideAmountText( + amount: balance, + style: const TextStyle( + fontSize: 35, + color: Colors.white, + fontFamily: "Satoshi Bold"), + textAlign: TextAlign.center, + ), + ], ), - Padding( - padding: EdgeInsets.only(left: 15, right: 15), - child: VerticalIconButton.extended( - onPressed: () => _presentQRReader(context), - icon: const Icon(Icons.qr_code, color: Colors.white), - label: S.of(context).pay_scan, - ), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.only(left: 16, right: 16, top: 10), + child: Row( + children: [ + if (isFiatServiceAvailable) ...[ + Padding( + padding: EdgeInsets.only(right: 10), + child: ActionButton( + icon: RealUnitTokenIcon(size: 20), + label: S.of(context).deposit, + onPressed: () => + getIt().launchProvider(context, true), ), - VerticalIconButton( - onPressed: () => context.push("/send"), - icon: const Icon(Icons.arrow_upward, color: Colors.white), - label: S.of(context).send, + ), + ActionButton( + icon: Icon( + Icons.account_balance, + color: Colors.white, + size: 20, ), - ], - ), - const SizedBox(height: 20), + label: S.of(context).withdraw, + onPressed: () => + getIt().launchProvider(context, false), + ), + ], ], ), ), - ); + ]); Future _presentQRReader(BuildContext context) async { QRData? result = await presentQRScanner( diff --git a/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart new file mode 100644 index 00000000..0f56950d --- /dev/null +++ b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:bitbox_flutter/bitbox_flutter.dart' as sdk; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:realunit_wallet/packages/hardware_wallet/bitbox.dart'; +import 'package:realunit_wallet/packages/service/wallet_service.dart'; +import 'package:realunit_wallet/packages/wallet/wallet.dart'; + +class ConnectBitboxCubit extends Cubit { + ConnectBitboxCubit(this._service, this._walletService) + : super(BitboxNotConnected()) { + _checkForTimer = + Timer.periodic(Duration(milliseconds: 50), (_) => checkForBitbox()); + } + + final BitboxService _service; + final WalletService _walletService; + Timer? _checkForTimer; + bool _isConnecting = false; + + Future checkForBitbox() async { + final devices = await _service.getAllUsbDevices(); + print(devices); + if (devices.isNotEmpty) { + emit(BitboxFound(devices.first)); + _checkForTimer?.cancel(); + connectToBitbox(devices.first); + } + } + + Future connectToBitbox(sdk.BitboxDevice device) async { + if (_isConnecting) return; + _isConnecting = true; + try { + await _service.connectDevice(device); + final wallet = await _walletService.createBitboxWallet("Luke-Skywallet"); + emit(BitboxConnected(wallet)); + } catch (_) { + emit(BitboxNotConnected()); + _checkForTimer = + Timer.periodic(Duration(milliseconds: 30), (_) => checkForBitbox()); + } + _isConnecting = false; + } + + @override + Future close() async { + _checkForTimer?.cancel(); + super.close(); + } +} + +abstract class BitboxConnectionState {} + +class BitboxNotConnected extends BitboxConnectionState {} + +class BitboxFound extends BitboxConnectionState { + final sdk.BitboxDevice device; + + BitboxFound(this.device); +} + +class BitboxConnected extends BitboxConnectionState { + final BitboxWallet wallet; + + BitboxConnected(this.wallet); +} diff --git a/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart b/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart new file mode 100644 index 00000000..b05da7bd --- /dev/null +++ b/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:realunit_wallet/di.dart'; +import 'package:realunit_wallet/packages/hardware_wallet/bitbox.dart'; +import 'package:realunit_wallet/packages/service/wallet_service.dart'; +import 'package:realunit_wallet/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart'; +import 'package:realunit_wallet/screens/hardware_connect_bitbox/connect_bitbox_view.dart'; + +class ConnectBitboxPage extends StatelessWidget { + const ConnectBitboxPage({super.key}); + + @override + Widget build(BuildContext context) => BlocProvider( + create: (_) => ConnectBitboxCubit(getIt(), getIt()), + child: ConnectBitboxView(), + ); +} diff --git a/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart b/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart new file mode 100644 index 00000000..966d4a21 --- /dev/null +++ b/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart @@ -0,0 +1,71 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:realunit_wallet/generated/i18n.dart'; +import 'package:realunit_wallet/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart'; +import 'package:realunit_wallet/screens/hardware_connect_bitbox/widgets/connect_content.dart'; +import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart'; +import 'package:realunit_wallet/styles/styles.dart'; +import 'package:realunit_wallet/widgets/handlebars.dart'; + +class ConnectBitboxView extends StatefulWidget { + const ConnectBitboxView({super.key}); + + @override + State createState() => _ConnectBitboxViewState(); +} + +class _ConnectBitboxViewState extends State { + @override + Widget build(BuildContext context) => BlocListener( + listener: (context, state) { + if (state is BitboxConnected) { + context.read().add(LoadWalletEvent(state.wallet)); + } + }, + child: Container( + color: Colors.white, + child: Column( + children: [ + Handlebars.horizontal(context, margin: EdgeInsets.only(top: 5), width: 36), + BlocBuilder( + builder: (context, state) => Stack(children: [ + AnimatedSlide( + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + offset: state is BitboxNotConnected ? Offset.zero : const Offset(-1.2, 0), + child: ConnectContent( + title: S.of(context).connect_bitbox_title, + content: Platform.isIOS + ? S.of(context).connect_bitbox_content_ios + : S.of(context).connect_bitbox_content, + imagePath: "assets/images/illustrations/bitbox_connect.svg", + ), + ), + AnimatedSlide( + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + offset: state is BitboxFound ? Offset.zero : const Offset(1.2, 0), + child: ConnectContent( + title: S.of(context).connected_bitbox_title, + content: S.of(context).connected_bitbox_content, + imagePath: "assets/images/illustrations/bitbox_connected.svg", + ), + ), + ]), + ), + Padding( + padding: const EdgeInsets.only(top: 28, bottom: 54), + child: ElevatedButton( + style: kFullwidthGrayButtonStyle, + onPressed: context.pop, + child: Text(S.of(context).cancel), + ), + ) + ], + ), + ), + ); +} diff --git a/lib/screens/hardware_connect_bitbox/widgets/connect_content.dart b/lib/screens/hardware_connect_bitbox/widgets/connect_content.dart new file mode 100644 index 00000000..772a24e7 --- /dev/null +++ b/lib/screens/hardware_connect_bitbox/widgets/connect_content.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:realunit_wallet/styles/styles.dart'; + +class ConnectContent extends StatelessWidget { + final String imagePath; + final String title; + final String content; + + const ConnectContent({ + super.key, + required this.imagePath, + required this.title, + required this.content, + }); + + @override + Widget build(BuildContext context) => Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 40, bottom: 20), + child: SvgPicture.asset(imagePath), + ), + Text( + title, + textAlign: TextAlign.center, + style: kBottonSheetTitleTextStyle, + ), + SizedBox( + width: 330, + child: Text( + content, + textAlign: TextAlign.center, + style: kBottonSheetContentTextStyle, + ), + ), + ], + ); +} diff --git a/lib/screens/settings/settings_page.dart b/lib/screens/settings/settings_page.dart index e5791a37..feea6868 100644 --- a/lib/screens/settings/settings_page.dart +++ b/lib/screens/settings/settings_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:realunit_wallet/di.dart'; import 'package:realunit_wallet/generated/i18n.dart'; +import 'package:realunit_wallet/packages/wallet/wallet.dart'; import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart'; import 'package:realunit_wallet/screens/settings/bloc/settings_bloc.dart'; import 'package:realunit_wallet/screens/settings/widgets/settings_section.dart'; @@ -76,12 +77,14 @@ class SettingsPage extends StatelessWidget { trailing: _forwardIcon, onTap: null, ), - SettingOption( - title: S.of(context).settings_wallet_backup, - leading: KeySolidIcon(size: 24), - trailing: _forwardIcon, - onTap: () => context.push('/settings/seed'), - ), + if (context.read().state.openWallet?.walletType == + WalletType.software) + SettingOption( + title: S.of(context).settings_wallet_backup, + leading: KeySolidIcon(size: 24), + trailing: _forwardIcon, + onTap: () => context.push('/settings/seed'), + ), ], ), ), diff --git a/lib/screens/welcome/welcome_page.dart b/lib/screens/welcome/welcome_page.dart index baafaca7..c3fae1cf 100644 --- a/lib/screens/welcome/welcome_page.dart +++ b/lib/screens/welcome/welcome_page.dart @@ -2,11 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:realunit_wallet/generated/i18n.dart'; +import 'package:realunit_wallet/router.dart'; +import 'package:realunit_wallet/screens/hardware_connect_bitbox/connect_bitbox_page.dart'; +import 'package:realunit_wallet/screens/welcome/widgets/welcome_card.dart'; import 'package:realunit_wallet/styles/colors.dart'; import 'package:realunit_wallet/styles/icons.dart'; -import 'widgets/welcome_card.dart'; - class WelcomePage extends StatefulWidget { const WelcomePage({super.key}); @@ -23,13 +24,8 @@ class _WelcomePageState extends State { appBar: AppBar( leading: showSecondStep ? IconButton( - onPressed: () => setState( - () => showSecondStep = false, - ), - icon: Icon( - Icons.arrow_back_rounded, - size: 24, - ), + onPressed: () => setState(() => showSecondStep = false), + icon: Icon(Icons.arrow_back_rounded, size: 24), ) : null, ), @@ -134,4 +130,15 @@ class _WelcomePageState extends State { ), ), ); + + void onBitboxPressed() { + showModalBottomSheet( + context: navigatorKey.currentContext!, + backgroundColor: Colors.white, + builder: (_) => BottomSheet( + onClosing: () {}, + builder: (_) => ConnectBitboxPage(), + ), + ); + } } diff --git a/lib/styles/colors.dart b/lib/styles/colors.dart index a30dfa08..b3575098 100644 --- a/lib/styles/colors.dart +++ b/lib/styles/colors.dart @@ -17,6 +17,7 @@ class RealUnitColors { static const brand600 = Color.fromARGB(255, 25, 136, 198); static const darkBlue = Color.fromARGB(255, 3, 76, 129); static const green = Color.fromARGB(255, 76, 172, 54); + static const neutral900 = Color.fromARGB(255, 15, 23, 42); static const neutral500 = Color.fromARGB(255, 100, 116, 139); static const neutral400 = Color.fromARGB(255, 148, 163, 184); static const neutral200 = Color.fromARGB(255, 226, 232, 240); diff --git a/lib/styles/styles.dart b/lib/styles/styles.dart index 193b691a..bce0f966 100644 --- a/lib/styles/styles.dart +++ b/lib/styles/styles.dart @@ -25,14 +25,21 @@ final kFullwidthPrimaryButtonStyle = ElevatedButton.styleFrom( ); final kFullwidthGrayButtonStyle = ElevatedButton.styleFrom( - backgroundColor: DEuroColors.neutralGrey, - fixedSize: Size(double.infinity, 55), + backgroundColor: RealUnitColors.neutral100, + fixedSize: const Size(double.infinity, 20), elevation: 0.0, + textStyle: kFullwidthGrayButtonTextStyle, +); + +const kFullwidthGrayButtonTextStyle = TextStyle( + fontSize: 16, + color: RealUnitColors.neutral900, + fontWeight: FontWeight.w600, ); final kFullwidthBlueButtonStyle = FilledButton.styleFrom( backgroundColor: RealUnitColors.realUnitBlue, - fixedSize: const Size(double.infinity, 50), + fixedSize: const Size(double.infinity, 20), padding: const EdgeInsets.only(left: 24, right: 24), ); @@ -60,3 +67,15 @@ const kContainerCardStyle = BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(12)), ); + +const kBottonSheetTitleTextStyle = TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: RealUnitColors.realUnitBlack, +); + +const kBottonSheetContentTextStyle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: RealUnitColors.neutral500, +); diff --git a/lib/widgets/handlebars.dart b/lib/widgets/handlebars.dart index 3987b64e..f67c4aa8 100644 --- a/lib/widgets/handlebars.dart +++ b/lib/widgets/handlebars.dart @@ -1,19 +1,20 @@ -import 'package:realunit_wallet/styles/colors.dart'; import 'package:flutter/material.dart'; +import 'package:realunit_wallet/styles/colors.dart'; class Handlebars { static Widget horizontal( BuildContext context, { EdgeInsetsGeometry margin = const EdgeInsets.only(top: 10), double? width, + double borderRadius = 5, }) => Container( margin: margin, height: 5, - width: width ??= MediaQuery.of(context).size.width * 0.25, + width: width ?? MediaQuery.of(context).size.width * 0.25, decoration: BoxDecoration( color: RealUnitColors.realUnitBlack, - borderRadius: BorderRadius.circular(5.0), + borderRadius: BorderRadius.circular(borderRadius), ), ); } diff --git a/lib/widgets/secondary_button.dart b/lib/widgets/secondary_button.dart new file mode 100644 index 00000000..a8e3a2d4 --- /dev/null +++ b/lib/widgets/secondary_button.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:realunit_wallet/styles/styles.dart'; + +class PrimaryButton extends StatelessWidget { + const PrimaryButton({ + super.key, + required this.label, + this.onPressed, + }); + + final String label; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) => ElevatedButton( + onPressed: onPressed, + style: kFullwidthGrayButtonStyle, + child: Text( + label, + textAlign: TextAlign.center, + style: kPrimaryButtonTextStyle, + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index 9834c497..b5eaa0e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + bitbox_flutter: + dependency: "direct main" + description: + path: "." + ref: e33e3888a5f384d960b11ad406b906b5770aede7 + resolved-ref: e33e3888a5f384d960b11ad406b906b5770aede7 + url: "https://github.com/konstantinullrich/bitbox_flutter" + source: git + version: "0.0.1" bloc: dependency: transitive description: @@ -1398,11 +1407,12 @@ packages: web3dart: dependency: "direct main" description: - name: web3dart - sha256: "885e5e8f0cc3c87c09f160a7fce6279226ca41316806f7ece2001959c62ecced" - url: "https://pub.dev" - source: hosted - version: "2.7.3" + path: "." + ref: cake + resolved-ref: aa3f932dbf54eda651b7bd01ad00204acf998bd0 + url: "https://github.com/cake-tech/web3dart.git" + source: git + version: "2.7.2" web_socket: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 93606bd9..b793b0e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,10 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + bitbox_flutter: + git: + url: https://github.com/konstantinullrich/bitbox_flutter + ref: e33e3888a5f384d960b11ad406b906b5770aede7 @@ -89,6 +93,10 @@ dev_dependencies: dependency_overrides: collection: 1.19.0 + web3dart: + git: + url: https://github.com/cake-tech/web3dart.git + ref: cake flutter: @@ -150,4 +158,4 @@ flutter_native_splash: fullscreen: false android_12: color: "#ffffff" - image: assets/images/splash/splash_logo.png \ No newline at end of file + image: assets/images/splash/splash_logo.png From 0f232fb3936143b8e9f69f02c44ee459bb1654b5 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Sun, 7 Dec 2025 13:36:03 +0100 Subject: [PATCH 3/9] refactor: rename Wallet to SoftwareWallet --- lib/packages/service/wallet_service.dart | 8 +- lib/packages/wallet/wallet.dart | 4 +- .../bloc/create_wallet_cubit.dart | 2 +- .../bloc/create_wallet_state.dart | 4 +- .../bloc/connect_bitbox_cubit.dart | 27 ++---- .../bloc/connect_bitbox_state.dart | 21 +++++ .../connect_bitbox_page.dart | 10 ++- .../connect_bitbox_view.dart | 89 ++++++++----------- .../bloc/restore_wallet_cubit.dart | 43 ++++----- .../bloc/restore_wallet_state.dart | 23 +++++ .../settings_seed/settings_seed_page.dart | 3 +- lib/widgets/secondary_button.dart | 24 ----- 12 files changed, 121 insertions(+), 137 deletions(-) create mode 100644 lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_state.dart create mode 100644 lib/screens/restore_wallet/bloc/restore_wallet_state.dart delete mode 100644 lib/widgets/secondary_button.dart diff --git a/lib/packages/service/wallet_service.dart b/lib/packages/service/wallet_service.dart index e9e5ca95..80d1b4b7 100644 --- a/lib/packages/service/wallet_service.dart +++ b/lib/packages/service/wallet_service.dart @@ -11,7 +11,7 @@ class WalletService{ const WalletService(this._bitboxService, this._repository, this._settingsRepository); - Future createWallet(String name) { + Future createSeedWallet(String name) { final mnemonic = bip39.generateMnemonic(); return restoreWallet(name, mnemonic); } @@ -23,10 +23,10 @@ class WalletService{ return BitboxWallet(walletId, name, address, _bitboxService); } - Future restoreWallet(String name, String seed) async { + Future restoreWallet(String name, String seed) async { final walletId = await _repository.createWallet(name, WalletType.software, seed); await _settingsRepository.saveCurrentWalletId(walletId); - return Wallet(walletId, name, seed); + return SoftwareWallet(walletId, name, seed); } Future getWalletById(int id) async { @@ -34,7 +34,7 @@ class WalletService{ final walletType = WalletType.values[result.type]; switch (walletType) { case WalletType.software: - return Wallet(result.id, result.name, result.seed); + return SoftwareWallet(result.id, result.name, result.seed); case WalletType.bitbox: return BitboxWallet( result.id, result.name, result.address, _bitboxService); diff --git a/lib/packages/wallet/wallet.dart b/lib/packages/wallet/wallet.dart index 6077a349..b35fb5f2 100644 --- a/lib/packages/wallet/wallet.dart +++ b/lib/packages/wallet/wallet.dart @@ -17,7 +17,7 @@ abstract class AWallet { AWallet(this.id, this.name); } -class Wallet extends AWallet { +class SoftwareWallet extends AWallet { @override WalletType get walletType => WalletType.software; @@ -32,7 +32,7 @@ class Wallet extends AWallet { @override WalletAccount get currentAccount => _currentAccount; - Wallet(super.id, super.name, this.seed) { + SoftwareWallet(super.id, super.name, this.seed) { final seedBytes = mnemonicToSeed(seed); _bip32 = BIP32.fromSeed(seedBytes); primaryAccount = WalletAccount(_bip32, 0); diff --git a/lib/screens/create_wallet/bloc/create_wallet_cubit.dart b/lib/screens/create_wallet/bloc/create_wallet_cubit.dart index 60c315b8..f3716873 100644 --- a/lib/screens/create_wallet/bloc/create_wallet_cubit.dart +++ b/lib/screens/create_wallet/bloc/create_wallet_cubit.dart @@ -10,7 +10,7 @@ class CreateWalletCubit extends Cubit { final WalletService _service; void createWallet() async { - final wallet = await _service.createWallet("Obi-Wallet-Kenobi"); + final wallet = await _service.createSeedWallet("Obi-Wallet-Kenobi"); emit(state.copyWith(wallet: wallet)); } diff --git a/lib/screens/create_wallet/bloc/create_wallet_state.dart b/lib/screens/create_wallet/bloc/create_wallet_state.dart index 055230cd..d5d776b0 100644 --- a/lib/screens/create_wallet/bloc/create_wallet_state.dart +++ b/lib/screens/create_wallet/bloc/create_wallet_state.dart @@ -4,11 +4,11 @@ final class CreateWalletState { const CreateWalletState({this.hideSeed = true, this.wallet}); final bool hideSeed; - final Wallet? wallet; + final SoftwareWallet? wallet; CreateWalletState copyWith({ bool? hideSeed, - Wallet? wallet, + SoftwareWallet? wallet, }) => CreateWalletState( hideSeed: hideSeed ?? this.hideSeed, diff --git a/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart index 0f56950d..d99df6e8 100644 --- a/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart +++ b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart @@ -6,21 +6,21 @@ import 'package:realunit_wallet/packages/hardware_wallet/bitbox.dart'; import 'package:realunit_wallet/packages/service/wallet_service.dart'; import 'package:realunit_wallet/packages/wallet/wallet.dart'; +part 'connect_bitbox_state.dart'; + class ConnectBitboxCubit extends Cubit { ConnectBitboxCubit(this._service, this._walletService) : super(BitboxNotConnected()) { _checkForTimer = - Timer.periodic(Duration(milliseconds: 50), (_) => checkForBitbox()); + Timer.periodic(Duration(milliseconds: 500), (_) => checkForBitbox()); } final BitboxService _service; final WalletService _walletService; Timer? _checkForTimer; - bool _isConnecting = false; Future checkForBitbox() async { final devices = await _service.getAllUsbDevices(); - print(devices); if (devices.isNotEmpty) { emit(BitboxFound(devices.first)); _checkForTimer?.cancel(); @@ -29,8 +29,8 @@ class ConnectBitboxCubit extends Cubit { } Future connectToBitbox(sdk.BitboxDevice device) async { - if (_isConnecting) return; - _isConnecting = true; + if (state is BitboxConnecting) return; + emit(BitboxConnecting(device)); try { await _service.connectDevice(device); final wallet = await _walletService.createBitboxWallet("Luke-Skywallet"); @@ -40,7 +40,6 @@ class ConnectBitboxCubit extends Cubit { _checkForTimer = Timer.periodic(Duration(milliseconds: 30), (_) => checkForBitbox()); } - _isConnecting = false; } @override @@ -49,19 +48,3 @@ class ConnectBitboxCubit extends Cubit { super.close(); } } - -abstract class BitboxConnectionState {} - -class BitboxNotConnected extends BitboxConnectionState {} - -class BitboxFound extends BitboxConnectionState { - final sdk.BitboxDevice device; - - BitboxFound(this.device); -} - -class BitboxConnected extends BitboxConnectionState { - final BitboxWallet wallet; - - BitboxConnected(this.wallet); -} diff --git a/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_state.dart b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_state.dart new file mode 100644 index 00000000..6a79d6e6 --- /dev/null +++ b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_state.dart @@ -0,0 +1,21 @@ +part of 'connect_bitbox_cubit.dart'; + +abstract class BitboxConnectionState {} + +class BitboxNotConnected extends BitboxConnectionState {} + +class BitboxFound extends BitboxConnectionState { + final sdk.BitboxDevice device; + + BitboxFound(this.device); +} + +class BitboxConnecting extends BitboxFound { + BitboxConnecting(super.device); +} + +class BitboxConnected extends BitboxConnectionState { + final BitboxWallet wallet; + + BitboxConnected(this.wallet); +} diff --git a/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart b/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart index b05da7bd..623b903b 100644 --- a/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart +++ b/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart @@ -5,6 +5,7 @@ import 'package:realunit_wallet/packages/hardware_wallet/bitbox.dart'; import 'package:realunit_wallet/packages/service/wallet_service.dart'; import 'package:realunit_wallet/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart'; import 'package:realunit_wallet/screens/hardware_connect_bitbox/connect_bitbox_view.dart'; +import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart'; class ConnectBitboxPage extends StatelessWidget { const ConnectBitboxPage({super.key}); @@ -12,6 +13,13 @@ class ConnectBitboxPage extends StatelessWidget { @override Widget build(BuildContext context) => BlocProvider( create: (_) => ConnectBitboxCubit(getIt(), getIt()), - child: ConnectBitboxView(), + child: BlocListener( + listener: (context, state) { + if (state is BitboxConnected) { + context.read().add(LoadWalletEvent(state.wallet)); + } + }, + child: ConnectBitboxView(), + ), ); } diff --git a/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart b/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart index 966d4a21..1fcbd18a 100644 --- a/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart +++ b/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart @@ -6,66 +6,53 @@ import 'package:go_router/go_router.dart'; import 'package:realunit_wallet/generated/i18n.dart'; import 'package:realunit_wallet/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart'; import 'package:realunit_wallet/screens/hardware_connect_bitbox/widgets/connect_content.dart'; -import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart'; import 'package:realunit_wallet/styles/styles.dart'; import 'package:realunit_wallet/widgets/handlebars.dart'; -class ConnectBitboxView extends StatefulWidget { +class ConnectBitboxView extends StatelessWidget { const ConnectBitboxView({super.key}); @override - State createState() => _ConnectBitboxViewState(); -} - -class _ConnectBitboxViewState extends State { - @override - Widget build(BuildContext context) => BlocListener( - listener: (context, state) { - if (state is BitboxConnected) { - context.read().add(LoadWalletEvent(state.wallet)); - } - }, - child: Container( - color: Colors.white, - child: Column( - children: [ - Handlebars.horizontal(context, margin: EdgeInsets.only(top: 5), width: 36), - BlocBuilder( - builder: (context, state) => Stack(children: [ - AnimatedSlide( - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - offset: state is BitboxNotConnected ? Offset.zero : const Offset(-1.2, 0), - child: ConnectContent( - title: S.of(context).connect_bitbox_title, - content: Platform.isIOS - ? S.of(context).connect_bitbox_content_ios - : S.of(context).connect_bitbox_content, - imagePath: "assets/images/illustrations/bitbox_connect.svg", - ), + Widget build(BuildContext context) => Container( + color: Colors.white, + child: Column( + children: [ + Handlebars.horizontal(context, margin: EdgeInsets.only(top: 5), width: 36), + BlocBuilder( + builder: (context, state) => Stack(children: [ + AnimatedSlide( + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + offset: state is BitboxNotConnected ? Offset.zero : const Offset(-1.2, 0), + child: ConnectContent( + title: S.of(context).connect_bitbox_title, + content: Platform.isIOS + ? S.of(context).connect_bitbox_content_ios + : S.of(context).connect_bitbox_content, + imagePath: "assets/images/illustrations/bitbox_connect.svg", ), - AnimatedSlide( - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - offset: state is BitboxFound ? Offset.zero : const Offset(1.2, 0), - child: ConnectContent( - title: S.of(context).connected_bitbox_title, - content: S.of(context).connected_bitbox_content, - imagePath: "assets/images/illustrations/bitbox_connected.svg", - ), + ), + AnimatedSlide( + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + offset: state is BitboxFound ? Offset.zero : const Offset(1.2, 0), + child: ConnectContent( + title: S.of(context).connected_bitbox_title, + content: S.of(context).connected_bitbox_content, + imagePath: "assets/images/illustrations/bitbox_connected.svg", ), - ]), - ), - Padding( - padding: const EdgeInsets.only(top: 28, bottom: 54), - child: ElevatedButton( - style: kFullwidthGrayButtonStyle, - onPressed: context.pop, - child: Text(S.of(context).cancel), ), - ) - ], - ), + ]), + ), + Padding( + padding: const EdgeInsets.only(top: 28, bottom: 54), + child: ElevatedButton( + style: kFullwidthGrayButtonStyle, + onPressed: context.pop, + child: Text(S.of(context).cancel), + ), + ) + ], ), ); } diff --git a/lib/screens/restore_wallet/bloc/restore_wallet_cubit.dart b/lib/screens/restore_wallet/bloc/restore_wallet_cubit.dart index 495449bf..f5b34d57 100644 --- a/lib/screens/restore_wallet/bloc/restore_wallet_cubit.dart +++ b/lib/screens/restore_wallet/bloc/restore_wallet_cubit.dart @@ -1,58 +1,46 @@ import 'package:bip39/src/wordlists/english.dart' as wordlist; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/packages/service/wallet_service.dart'; import 'package:realunit_wallet/packages/wallet/seedqr.dart'; import 'package:realunit_wallet/packages/wallet/wallet.dart'; import 'package:realunit_wallet/widgets/qr_scanner.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -class RestoreWalletState { - final bool isSeedReady; - final bool isLoading; - final bool isRestored; - final Wallet? wallet; - - RestoreWalletState(this.isSeedReady, this.isLoading, this.isRestored, - [this.wallet]); -} +part 'restore_wallet_state.dart'; class RestoreWalletCubit extends Cubit { - RestoreWalletCubit(this._walletService) - : super(RestoreWalletState(false, false, false)); + RestoreWalletCubit(this._walletService) : super(RestoreWalletState(false, false, false)); final WalletService _walletService; void restoreWallet(String seed) async { - emit(RestoreWalletState(state.isSeedReady, true, false)); + emit(state.copyWith(isLoading: true)); - final normalizedSeed = - seed.split(" ").where((element) => element.isNotEmpty).join(" "); + final normalizedSeed = seed.split(" ").where((element) => element.isNotEmpty).join(" "); - final wallet = - await _walletService.restoreWallet("Obi-Wallet-Kenobi", normalizedSeed); + final wallet = await _walletService.restoreWallet("Obi-Wallet-Kenobi", normalizedSeed); - emit(RestoreWalletState(true, false, true, wallet)); + emit(state.copyWith(isSeedReady: true, isLoading: false, isRestored: true, wallet: wallet)); } void validateSeed(String seed) { final seedWords = seed.split(" ").where((element) => element.isNotEmpty); if (seedWords.length == 12 && _containsAll(wordlist.WORDLIST, seedWords)) { - emit(RestoreWalletState(true, state.isLoading, false)); + emit(state.copyWith(isSeedReady: true)); } else { - emit(RestoreWalletState(false, state.isLoading, false)); + emit(state.copyWith(isSeedReady: false)); } } Future restoreWalletFromSeedQR(BuildContext context) async { if (context.mounted) { - emit(RestoreWalletState(state.isSeedReady, true, false)); + emit(state.copyWith(isLoading: true)); final data = await presentQRScanner( context, (String? code, List? rawBytes) => - rawBytes?.isNotEmpty == true && isSeedQr(code ?? "") || - isCompactSeedQr(rawBytes ?? []), + rawBytes?.isNotEmpty == true && isSeedQr(code ?? "") || isCompactSeedQr(rawBytes ?? []), ); String? seed; @@ -63,11 +51,10 @@ class RestoreWalletCubit extends Cubit { } if (seed != null) { - final wallet = - await _walletService.restoreWallet("Obi-Wallet-Kenobi", seed); - emit(RestoreWalletState(true, false, true, wallet)); + final wallet = await _walletService.restoreWallet("Obi-Wallet-Kenobi", seed); + emit(state.copyWith(isSeedReady: true, isLoading: false, isRestored: true, wallet: wallet)); } else { - emit(RestoreWalletState(state.isSeedReady, false, false)); + emit(state.copyWith(isLoading: false, isRestored: false)); } } } diff --git a/lib/screens/restore_wallet/bloc/restore_wallet_state.dart b/lib/screens/restore_wallet/bloc/restore_wallet_state.dart new file mode 100644 index 00000000..be2c28d0 --- /dev/null +++ b/lib/screens/restore_wallet/bloc/restore_wallet_state.dart @@ -0,0 +1,23 @@ +part of 'restore_wallet_cubit.dart'; + +class RestoreWalletState { + final bool isSeedReady; + final bool isLoading; + final bool isRestored; + final SoftwareWallet? wallet; + + const RestoreWalletState(this.isSeedReady, this.isLoading, this.isRestored, [this.wallet]); + + RestoreWalletState copyWith({ + bool? isSeedReady, + bool? isLoading, + bool? isRestored, + SoftwareWallet? wallet, + }) => + RestoreWalletState( + isSeedReady ?? this.isSeedReady, + isLoading ?? this.isLoading, + isRestored ?? this.isRestored, + wallet ?? this.wallet, + ); +} diff --git a/lib/screens/settings_seed/settings_seed_page.dart b/lib/screens/settings_seed/settings_seed_page.dart index 919c3508..50375b47 100644 --- a/lib/screens/settings_seed/settings_seed_page.dart +++ b/lib/screens/settings_seed/settings_seed_page.dart @@ -11,8 +11,7 @@ class SettingsSeedPage extends StatelessWidget { @override Widget build(BuildContext context) => BlocProvider( - create: (_) => - SettingsSeedCubit((getIt().wallet as Wallet).seed), + create: (_) => SettingsSeedCubit((getIt().wallet as SoftwareWallet).seed), child: SettingsSeedView(), ); } diff --git a/lib/widgets/secondary_button.dart b/lib/widgets/secondary_button.dart deleted file mode 100644 index a8e3a2d4..00000000 --- a/lib/widgets/secondary_button.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:realunit_wallet/styles/styles.dart'; - -class PrimaryButton extends StatelessWidget { - const PrimaryButton({ - super.key, - required this.label, - this.onPressed, - }); - - final String label; - final VoidCallback? onPressed; - - @override - Widget build(BuildContext context) => ElevatedButton( - onPressed: onPressed, - style: kFullwidthGrayButtonStyle, - child: Text( - label, - textAlign: TextAlign.center, - style: kPrimaryButtonTextStyle, - ), - ); -} From 5f583e43be217a9507836b1490108578031219c7 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 10 Dec 2025 14:59:40 +0100 Subject: [PATCH 4/9] Update lib/styles/styles.dart Co-authored-by: Lam Nguyen <32935491+xlamn@users.noreply.github.com> --- lib/styles/styles.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/styles/styles.dart b/lib/styles/styles.dart index bce0f966..3699ed7a 100644 --- a/lib/styles/styles.dart +++ b/lib/styles/styles.dart @@ -74,7 +74,7 @@ const kBottonSheetTitleTextStyle = TextStyle( color: RealUnitColors.realUnitBlack, ); -const kBottonSheetContentTextStyle = TextStyle( +const kBottomSheetContentTextStyle = TextStyle( fontSize: 14, fontWeight: FontWeight.w400, color: RealUnitColors.neutral500, From 75329526b525d141249e9dd07fcd976ecc02ff67 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 10 Dec 2025 14:59:48 +0100 Subject: [PATCH 5/9] Update lib/styles/styles.dart Co-authored-by: Lam Nguyen <32935491+xlamn@users.noreply.github.com> --- lib/styles/styles.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/styles/styles.dart b/lib/styles/styles.dart index 3699ed7a..1064c185 100644 --- a/lib/styles/styles.dart +++ b/lib/styles/styles.dart @@ -68,7 +68,7 @@ const kContainerCardStyle = BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(12)), ); -const kBottonSheetTitleTextStyle = TextStyle( +const kBottomSheetTitleTextStyle = TextStyle( fontSize: 22, fontWeight: FontWeight.w700, color: RealUnitColors.realUnitBlack, From c6a893e5bab50496493cc71dc11cc416cae8df23 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 10 Dec 2025 15:03:25 +0100 Subject: [PATCH 6/9] fix: correct typo in BottomSheet text style constants --- .../hardware_connect_bitbox/widgets/connect_content.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/hardware_connect_bitbox/widgets/connect_content.dart b/lib/screens/hardware_connect_bitbox/widgets/connect_content.dart index 772a24e7..c690f48a 100644 --- a/lib/screens/hardware_connect_bitbox/widgets/connect_content.dart +++ b/lib/screens/hardware_connect_bitbox/widgets/connect_content.dart @@ -24,14 +24,14 @@ class ConnectContent extends StatelessWidget { Text( title, textAlign: TextAlign.center, - style: kBottonSheetTitleTextStyle, + style: kBottomSheetTitleTextStyle, ), SizedBox( width: 330, child: Text( content, textAlign: TextAlign.center, - style: kBottonSheetContentTextStyle, + style: kBottomSheetContentTextStyle, ), ), ], From e30e7f4dc07e9e7efd4eadb2d53c7ecb3a9cb890 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 10 Dec 2025 15:46:37 +0100 Subject: [PATCH 7/9] refactor: use `defaultTargetPlatform` for platform checks and conditionally display BitBox content --- assets/languages/strings_de.arb | 6 +++--- assets/languages/strings_en.arb | 6 +++--- lib/packages/utils/device_info.dart | 8 ++++---- .../connect_bitbox_view.dart | 5 ++--- lib/screens/welcome/welcome_page.dart | 14 ++++++++------ 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/assets/languages/strings_de.arb b/assets/languages/strings_de.arb index f73c4ed1..e1b16eaa 100644 --- a/assets/languages/strings_de.arb +++ b/assets/languages/strings_de.arb @@ -17,17 +17,17 @@ "buy_payment_not_possible": "Kauf momentan nicht möglich.", "buy_payment_not_possible_description": "Es scheint ein Berechtigungsproblem vorzuliegen. Kontaktieren Sie bitte den Support für weitere Informationen.", "buy_realu": "REALU kaufen", - "close": "Schließen", "cancel": "Abbrechen", + "close": "Schließen", "collect_interest": "Auszahlen", "confirm": "Ausführen", "connect_bitbox_content": "Bitte verbinden Sie Ihre BitBox02 mit Ihrem Smartphone.", "connect_bitbox_content_ios": "Bitte verbinden Sie Ihre BitBox02 mit Ihrem Smartphone und aktivieren Sie zusätzlich Bluetooth.", "connect_bitbox_title": "BitBox02 verbinden", "connect_with": "Mit ${wallet} verbinden", - "contact_support": "Support kontaktieren", "connected_bitbox_content": "Bitte folgen Sie nun den Anweisungen auf Ihrer BitBox02.", "connected_bitbox_title": "Verbindung erfolgreich", + "contact_support": "Support kontaktieren", "copy_seed": "Seed kopieren", "country": "Land", "create_wallet": "Neue Wallet erstellen", @@ -126,4 +126,4 @@ "your_deuro_cash_and_savings_wallet": "Dein dEURO Cash\n& Spar Wallet", "your_seed": "Dein dEURO-Seed", "your_seed_disclaimer": "Dies ist Ihr einzigartiger und privater Seed und der einzige Weg, um Ihre Wallet im Falle eines Verlusts oder einer Fehlfunktion wiederherzustellen." -} +} \ No newline at end of file diff --git a/assets/languages/strings_en.arb b/assets/languages/strings_en.arb index 1a6bc73b..eb080512 100644 --- a/assets/languages/strings_en.arb +++ b/assets/languages/strings_en.arb @@ -17,17 +17,17 @@ "buy_payment_not_possible": "Purchase currently not possible.", "buy_payment_not_possible_description": "There appears to be a permission issue. Please contact support for further information.", "buy_realu": "Buy REALU", - "close": "Close", "cancel": "Cancel", + "close": "Close", "collect_interest": "Collect", "confirm": "Confirm", "connect_bitbox_content": "Please connect your BitBox02 with your Smartphone.", "connect_bitbox_content_ios": "Please connect your BitBox02 with your Smartphone and activate Bluetooth.", "connect_bitbox_title": "Connect BitBox02", "connect_with": "Connect with ${wallet}", - "contact_support": "Contact support", "connected_bitbox_content": "Please follow the steps on your BitBox02.", "connected_bitbox_title": "Connection successful", + "contact_support": "Contact support", "copy_seed": "Copy seed", "country": "Country", "create_wallet": "Create new Wallet", @@ -126,4 +126,4 @@ "your_deuro_cash_and_savings_wallet": "Your dEURO Cash\n& Savings Wallet", "your_seed": "Your dEURO-seed", "your_seed_disclaimer": "This is your unique and private seed and it is the only way to recover your wallet in case of loss or malfunction." -} +} \ No newline at end of file diff --git a/lib/packages/utils/device_info.dart b/lib/packages/utils/device_info.dart index c5295b0d..0b15674d 100644 --- a/lib/packages/utils/device_info.dart +++ b/lib/packages/utils/device_info.dart @@ -1,12 +1,12 @@ -import 'dart:io'; +import 'package:flutter/foundation.dart'; class DeviceInfo { DeviceInfo._(); static DeviceInfo get instance => DeviceInfo._(); - bool get isMobile => Platform.isAndroid || Platform.isIOS; + bool get isMobile => [TargetPlatform.android, TargetPlatform.iOS].contains(defaultTargetPlatform); - bool get isDesktop => - Platform.isMacOS || Platform.isWindows || Platform.isLinux; + bool get isDesktop => [TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux] + .contains(defaultTargetPlatform); } diff --git a/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart b/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart index 1fcbd18a..9f83f13b 100644 --- a/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart +++ b/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart @@ -1,5 +1,4 @@ -import 'dart:io'; - +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -26,7 +25,7 @@ class ConnectBitboxView extends StatelessWidget { offset: state is BitboxNotConnected ? Offset.zero : const Offset(-1.2, 0), child: ConnectContent( title: S.of(context).connect_bitbox_title, - content: Platform.isIOS + content: defaultTargetPlatform == TargetPlatform.iOS ? S.of(context).connect_bitbox_content_ios : S.of(context).connect_bitbox_content, imagePath: "assets/images/illustrations/bitbox_connect.svg", diff --git a/lib/screens/welcome/welcome_page.dart b/lib/screens/welcome/welcome_page.dart index c3fae1cf..49d84d9a 100644 --- a/lib/screens/welcome/welcome_page.dart +++ b/lib/screens/welcome/welcome_page.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; @@ -86,13 +87,14 @@ class _WelcomePageState extends State { 'assets/images/illustrations/software_wallet.svg', ), ), - WelcomeCard( - title: S.of(context).bitbox, - description: S.of(context).hardware_wallet_subtitle, - trailing: SvgPicture.asset( - 'assets/images/illustrations/bitbox.svg', + if (defaultTargetPlatform == TargetPlatform.android) + WelcomeCard( + title: S.of(context).bitbox, + description: S.of(context).hardware_wallet_subtitle, + trailing: SvgPicture.asset( + 'assets/images/illustrations/bitbox.svg', + ), ), - ), ], ), ), From f0f90ab00d8c1c02fb810cfddda8d2e1bc8ef61b Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 15 Dec 2025 12:13:00 +0100 Subject: [PATCH 8/9] refactor: remove unused `RestoreWalletState` and update `SoftwareWallet` references --- .../bloc/restore_wallet_cubit.dart | 0 .../bloc/restore_wallet_state.dart | 23 --------------- .../restore_wallet/restore_wallet_state.dart | 2 +- pubspec.lock | 28 +++++++++---------- pubspec.yaml | 2 +- .../restore_wallet_page_test.dart | 2 +- 6 files changed, 17 insertions(+), 40 deletions(-) delete mode 100644 lib/screens/restore_wallet/bloc/restore_wallet_cubit.dart delete mode 100644 lib/screens/restore_wallet/bloc/restore_wallet_state.dart diff --git a/lib/screens/restore_wallet/bloc/restore_wallet_cubit.dart b/lib/screens/restore_wallet/bloc/restore_wallet_cubit.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/screens/restore_wallet/bloc/restore_wallet_state.dart b/lib/screens/restore_wallet/bloc/restore_wallet_state.dart deleted file mode 100644 index be2c28d0..00000000 --- a/lib/screens/restore_wallet/bloc/restore_wallet_state.dart +++ /dev/null @@ -1,23 +0,0 @@ -part of 'restore_wallet_cubit.dart'; - -class RestoreWalletState { - final bool isSeedReady; - final bool isLoading; - final bool isRestored; - final SoftwareWallet? wallet; - - const RestoreWalletState(this.isSeedReady, this.isLoading, this.isRestored, [this.wallet]); - - RestoreWalletState copyWith({ - bool? isSeedReady, - bool? isLoading, - bool? isRestored, - SoftwareWallet? wallet, - }) => - RestoreWalletState( - isSeedReady ?? this.isSeedReady, - isLoading ?? this.isLoading, - isRestored ?? this.isRestored, - wallet ?? this.wallet, - ); -} diff --git a/lib/screens/restore_wallet/cubit/restore_wallet/restore_wallet_state.dart b/lib/screens/restore_wallet/cubit/restore_wallet/restore_wallet_state.dart index 9f8674a8..704eab3f 100644 --- a/lib/screens/restore_wallet/cubit/restore_wallet/restore_wallet_state.dart +++ b/lib/screens/restore_wallet/cubit/restore_wallet/restore_wallet_state.dart @@ -2,7 +2,7 @@ part of 'restore_wallet_cubit.dart'; class RestoreWalletState extends Equatable { final bool isLoading; - final Wallet? wallet; + final SoftwareWallet? wallet; const RestoreWalletState({ this.isLoading = false, diff --git a/pubspec.lock b/pubspec.lock index 73641e28..7f592961 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -811,26 +811,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" lints: dependency: transitive description: @@ -1312,26 +1312,26 @@ packages: dependency: transitive description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.25.15" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.11" + version: "0.6.8" timing: dependency: transitive description: @@ -1456,10 +1456,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 10a33260..bf3e0e0b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.0.1+1 +version: 0.0.1+2 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/test/screens/restore_wallet/restore_wallet_page_test.dart b/test/screens/restore_wallet/restore_wallet_page_test.dart index 476db0fc..f0066089 100644 --- a/test/screens/restore_wallet/restore_wallet_page_test.dart +++ b/test/screens/restore_wallet/restore_wallet_page_test.dart @@ -27,7 +27,7 @@ class MockHomeBloc extends MockBloc implements HomeBloc {} class MockWalletService extends Mock implements WalletService {} -class MockWallet extends Mock implements Wallet {} +class MockWallet extends Mock implements SoftwareWallet {} void main() { late RestoreWalletCubit restoreWalletCubit; From d7191c264534bb65b62f1bbe34777034fb208e81 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 15 Dec 2025 22:56:57 +0100 Subject: [PATCH 9/9] chore: update dependencies and improve README setup instructions --- README.md | 7 ++++++- pubspec.lock | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4e1d2a13..b212ffe7 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,11 @@ A Flutter Wallet for Real Unit Investors. ## Getting Started +Before getting started, please make sure you have the latest version of Flutter, golang and gomobile installed. +```shell +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +``` ### 1. Generate translations @@ -26,4 +31,4 @@ flutter pub get ```shell flutter run -``` \ No newline at end of file +``` diff --git a/pubspec.lock b/pubspec.lock index 7f592961..73641e28 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -811,26 +811,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -1312,26 +1312,26 @@ packages: dependency: transitive description: name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.25.15" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.11" timing: dependency: transitive description: @@ -1456,10 +1456,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: