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/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 05f13647..d639558c 100644 --- a/assets/languages/strings_de.arb +++ b/assets/languages/strings_de.arb @@ -17,10 +17,16 @@ "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", + "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", + "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", diff --git a/assets/languages/strings_en.arb b/assets/languages/strings_en.arb index f6158ca8..86e05fdd 100644 --- a/assets/languages/strings_en.arb +++ b/assets/languages/strings_en.arb @@ -17,10 +17,16 @@ "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", + "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}", + "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", diff --git a/lib/di.dart b/lib/di.dart index 78d50be3..f6c011f3 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'; @@ -76,19 +77,23 @@ 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())); @@ -97,8 +102,8 @@ void setupServices() { getIt.registerFactory(() => DfxBrokerbotService(getIt())); getIt.registerFactory(() => SettingsService(getIt())); - getIt.registerFactory( - () => DFXService(getIt(), getIt(), getIt())); + getIt.registerFactory(() => DFXService(getIt(), + getIt(), getIt())); } void setupBlocs() { @@ -112,4 +117,5 @@ void setupBlocs() { )); } -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 f58c4197..8c3223f0 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) { + Future createSeedWallet(String name) { final mnemonic = bip39.generateMnemonic(); return restoreWallet(name, mnemonic); } - Future restoreWallet(String name, String seed) async { - final walletId = await _repository.createWallet(name, seed); + 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 Wallet(walletId, name, seed); + return BitboxWallet(walletId, name, address, _bitboxService); } - Future getWalletById(int id) async { + Future restoreWallet(String name, String seed) async { + final walletId = await _repository.createWallet(name, WalletType.software, seed); + await _settingsRepository.saveCurrentWalletId(walletId); + return SoftwareWallet(walletId, name, seed); + } + + 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 SoftwareWallet(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/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/packages/wallet/wallet.dart b/lib/packages/wallet/wallet.dart index e998dc92..b35fb5f2 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 } @@ -16,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; @@ -31,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); @@ -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/create_wallet/bloc/create_wallet_cubit.dart b/lib/screens/create_wallet/bloc/create_wallet_cubit.dart index ce60c847..1cf86712 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/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..d99df6e8 --- /dev/null +++ b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart @@ -0,0 +1,50 @@ +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'; + +part 'connect_bitbox_state.dart'; + +class ConnectBitboxCubit extends Cubit { + ConnectBitboxCubit(this._service, this._walletService) + : super(BitboxNotConnected()) { + _checkForTimer = + Timer.periodic(Duration(milliseconds: 500), (_) => checkForBitbox()); + } + + final BitboxService _service; + final WalletService _walletService; + Timer? _checkForTimer; + + Future checkForBitbox() async { + final devices = await _service.getAllUsbDevices(); + if (devices.isNotEmpty) { + emit(BitboxFound(devices.first)); + _checkForTimer?.cancel(); + connectToBitbox(devices.first); + } + } + + Future connectToBitbox(sdk.BitboxDevice device) async { + if (state is BitboxConnecting) return; + emit(BitboxConnecting(device)); + 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()); + } + } + + @override + Future close() async { + _checkForTimer?.cancel(); + super.close(); + } +} 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 new file mode 100644 index 00000000..623b903b --- /dev/null +++ b/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart @@ -0,0 +1,25 @@ +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'; +import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart'; + +class ConnectBitboxPage extends StatelessWidget { + const ConnectBitboxPage({super.key}); + + @override + Widget build(BuildContext context) => BlocProvider( + create: (_) => ConnectBitboxCubit(getIt(), getIt()), + 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 new file mode 100644 index 00000000..9f83f13b --- /dev/null +++ b/lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart @@ -0,0 +1,57 @@ +import 'package:flutter/foundation.dart'; +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/styles/styles.dart'; +import 'package:realunit_wallet/widgets/handlebars.dart'; + +class ConnectBitboxView extends StatelessWidget { + const ConnectBitboxView({super.key}); + + @override + 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: defaultTargetPlatform == TargetPlatform.iOS + ? 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..c690f48a --- /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: kBottomSheetTitleTextStyle, + ), + SizedBox( + width: 330, + child: Text( + content, + textAlign: TextAlign.center, + style: kBottomSheetContentTextStyle, + ), + ), + ], + ); +} 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/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/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/screens/welcome/welcome_page.dart b/lib/screens/welcome/welcome_page.dart index baafaca7..49d84d9a 100644 --- a/lib/screens/welcome/welcome_page.dart +++ b/lib/screens/welcome/welcome_page.dart @@ -1,12 +1,14 @@ +import 'package:flutter/foundation.dart'; 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 +25,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, ), @@ -90,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', + ), ), - ), ], ), ), @@ -134,4 +132,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 9f5a5ba1..e5291deb 100644 --- a/lib/styles/colors.dart +++ b/lib/styles/colors.dart @@ -21,6 +21,7 @@ class RealUnitColors { static const green = Color.fromARGB(255, 76, 172, 54); static const okker = Color(0xFFE9AD3F); + 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 neutral300 = Color(0xFFCED5DE); diff --git a/lib/styles/styles.dart b/lib/styles/styles.dart index 193b691a..1064c185 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 kBottomSheetTitleTextStyle = TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: RealUnitColors.realUnitBlack, +); + +const kBottomSheetContentTextStyle = 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/pubspec.lock b/pubspec.lock index c265ffd2..73641e28 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: @@ -1486,11 +1495,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 b9d3eed7..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' @@ -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 @@ -90,6 +94,10 @@ dev_dependencies: dependency_overrides: collection: 1.19.0 + web3dart: + git: + url: https://github.com/cake-tech/web3dart.git + ref: cake flutter: @@ -151,4 +159,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 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;