diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 370b0abe..731360f9 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,6 +2,12 @@
+
+
+
bitboxManager!.signETHRLPTransaction(
+ chainId ?? 1,
+ derivationPath!,
+ bytesToHex(payload),
+ isEIP1559,
+ ),
);
final r = bytesToHex(sig.sublist(0, 32));
@@ -101,7 +103,9 @@ class BitboxCredentials extends CredentialsWithKnownAddress {
Future signPersonalMessage(Uint8List payload, {int? chainId}) {
return _synchronizeSign(() async {
if (!isConnected) throw const BitboxNotConnectedException();
- return await bitboxManager!.signETHMessage(chainId ?? 1, derivationPath!, payload);
+ return await _runOrThrowDisconnect(
+ () => bitboxManager!.signETHMessage(chainId ?? 1, derivationPath!, payload),
+ );
});
}
@@ -113,14 +117,43 @@ class BitboxCredentials extends CredentialsWithKnownAddress {
return _synchronizeSign(() async {
if (!isConnected) throw const BitboxNotConnectedException();
- final signatureBytes = await bitboxManager!.signETHTypedMessage(
- chainId,
- derivationPath!,
- Uint8List.fromList(utf8.encode(jsonData)),
+ final signatureBytes = await _runOrThrowDisconnect(
+ () => bitboxManager!.signETHTypedMessage(
+ chainId,
+ derivationPath!,
+ Uint8List.fromList(utf8.encode(jsonData)),
+ ),
);
return '0x${convert.hex.encode(signatureBytes)}';
});
}
+ /// Wrap a sign call so that a BLE/USB drop mid-operation surfaces as
+ /// [BitboxNotConnectedException] instead of a raw plugin error. If the
+ /// device is still reachable after the failure, the original error wins.
+ Future _runOrThrowDisconnect(Future Function() op) async {
+ try {
+ return await op();
+ } catch (_) {
+ if (await _deviceLost()) {
+ clearBitbox();
+ throw const BitboxNotConnectedException();
+ }
+ rethrow;
+ }
+ }
+
+ Future _deviceLost() async {
+ final manager = bitboxManager;
+ if (manager == null) return true;
+ try {
+ final devices = await manager.devices;
+ return devices.isEmpty;
+ } catch (_) {
+ // Probing the device list itself failed — treat as lost.
+ return true;
+ }
+ }
+
bool get isConnected => bitboxManager != null;
}
diff --git a/lib/packages/repository/wallet_repository.dart b/lib/packages/repository/wallet_repository.dart
index f04cc516..13b9d582 100644
--- a/lib/packages/repository/wallet_repository.dart
+++ b/lib/packages/repository/wallet_repository.dart
@@ -9,15 +9,33 @@ class WalletRepository {
const WalletRepository(this._appDatabase, this._secureStorage);
- Future createWallet(String name, WalletType type, String seed) async {
+ Future createWallet(
+ String name,
+ WalletType type,
+ String seed,
+ String address,
+ ) async {
final encryptedSeed = await _encryptSeed(seed);
- return _appDatabase.insertWallet(name, encryptedSeed, '', type.index);
+ return _appDatabase.insertWallet(name, encryptedSeed, address, type.index);
}
Future createViewWallet(String name, WalletType type, String address) =>
_appDatabase.insertWallet(name, '', address, type.index);
- Future getWalletById(int id) async {
+ /// Returns the wallet row with the encrypted seed *still encrypted*. Use this
+ /// at app startup so we don't pay the mnemonic-decrypt / BIP32-derivation
+ /// cost just to render the dashboard — the cached address is enough.
+ Future getWalletInfo(int id) => _appDatabase.getWalletById(id);
+
+ /// Backfills the address column for legacy software-wallet rows that were
+ /// created before address-caching landed. After this runs once, subsequent
+ /// loads of the same row stay on the fast view-wallet path.
+ Future updateAddress(int id, String address) =>
+ _appDatabase.updateWalletAddress(id, address);
+
+ /// Returns the wallet row with the seed decrypted. Only call this when the
+ /// private key is actually needed (signing a sell, revealing the seed).
+ Future getUnlockedWalletById(int id) async {
final info = await _appDatabase.getWalletById(id);
if (info == null) return null;
if (info.seed.isEmpty) return info;
diff --git a/lib/packages/service/app_store.dart b/lib/packages/service/app_store.dart
index 48e1fa2e..ef114750 100644
--- a/lib/packages/service/app_store.dart
+++ b/lib/packages/service/app_store.dart
@@ -10,6 +10,12 @@ class AppStore {
AWallet? _wallet;
+ /// Callback that decrypts the mnemonic and returns a fully unlocked
+ /// [SoftwareWallet]. Wired up after DI registers `WalletService`; null until
+ /// then. Used by [ensureUnlocked] so callers don't have to import the
+ /// service layer just to upgrade a view-wallet.
+ Future Function()? _unlocker;
+
AppStore(this.getApiConfig, this.sessionCache);
set wallet(AWallet wallet_) => _wallet = wallet_;
@@ -22,4 +28,19 @@ class AppStore {
ApiConfig get apiConfig => getApiConfig();
String get primaryAddress => wallet.currentAccount.primaryAddress.address.hex;
+
+ void attachUnlocker(Future Function() unlocker) {
+ _unlocker = unlocker;
+ }
+
+ /// Upgrades the current wallet from [SoftwareViewWallet] (address only) to a
+ /// fully unlocked [SoftwareWallet] (mnemonic in memory) so the next sign
+ /// operation can run. No-op for wallets that aren't locked, or when no
+ /// unlocker has been wired (e.g. tests).
+ Future ensureUnlocked() async {
+ if (_wallet is! SoftwareViewWallet) return;
+ final unlocker = _unlocker;
+ if (unlocker == null) return;
+ _wallet = await unlocker();
+ }
}
diff --git a/lib/packages/service/dfx/dfx_auth_service.dart b/lib/packages/service/dfx/dfx_auth_service.dart
index e6353471..033b6715 100644
--- a/lib/packages/service/dfx/dfx_auth_service.dart
+++ b/lib/packages/service/dfx/dfx_auth_service.dart
@@ -24,21 +24,42 @@ abstract class DFXAuthService {
String get walletAddress => wallet.primaryAddress.address.hexEip55;
- Future getSignMessage() async {
- final uri = buildUri(host, signMessagePath, {'address': walletAddress});
+ Future getSignMessage() => _fetchSignMessage(walletAddress);
+
+ /// Create-and-persist the auth signature for [account] without going through
+ /// `appStore.wallet`. Used during the BitBox pairing flow so the signature is
+ /// captured while the hardware wallet is guaranteed connected — every
+ /// subsequent buy / KYC / user-data call can then run off the cached
+ /// signature without needing the BitBox.
+ ///
+ /// No-op if a signature for this address is already in the cache.
+ Future ensureSignatureFor(AWalletAccount account) async {
+ final address = account.primaryAddress.address.hexEip55;
+ await appStore.sessionCache.loadSignature();
+ if (appStore.sessionCache.signature != null &&
+ appStore.sessionCache.signatureAddress == address) {
+ return;
+ }
+ final message = await _fetchSignMessage(address);
+ final signature = await account.signMessage(message).timeout(_signMessageTimeout);
+ if (signature.isEmpty || signature == '0x') {
+ throw const SigningCancelledException();
+ }
+ await appStore.sessionCache.saveSignature(address, signature);
+ }
+
+ Future _fetchSignMessage(String address) async {
+ final uri = buildUri(host, signMessagePath, {'address': address});
final response = await appStore.httpClient
.get(uri, headers: {'accept': 'application/json'})
.timeout(_httpTimeout);
-
- if (response.statusCode == 200) {
- final responseBody = jsonDecode(response.body);
- return responseBody['message'] as String;
- } else {
+ if (response.statusCode != 200) {
throw Exception(
'Failed to get sign message. Status: ${response.statusCode} ${response.body}',
);
}
+ return (jsonDecode(response.body) as Map)['message'] as String;
}
// Exceptions this method can throw on the BitBox path:
@@ -54,6 +75,9 @@ abstract class DFXAuthService {
return cached;
}
+ // Cache miss — we actually need the private key. Decrypt the mnemonic on
+ // demand if the currently loaded wallet is a view-only software wallet.
+ await appStore.ensureUnlocked();
final signature = await wallet.signMessage(message).timeout(_signMessageTimeout);
if (signature.isEmpty || signature == '0x') {
throw const SigningCancelledException();
diff --git a/lib/packages/service/dfx/exceptions/bitbox_exception.dart b/lib/packages/service/dfx/exceptions/bitbox_exception.dart
index 65d4bbeb..45462029 100644
--- a/lib/packages/service/dfx/exceptions/bitbox_exception.dart
+++ b/lib/packages/service/dfx/exceptions/bitbox_exception.dart
@@ -1,3 +1,6 @@
class BitboxNotConnectedException implements Exception {
const BitboxNotConnectedException();
+
+ @override
+ String toString() => 'BitBox is not connected';
}
diff --git a/lib/packages/service/dfx/models/payment/payment_info_error.dart b/lib/packages/service/dfx/models/payment/payment_info_error.dart
index 0c53cc40..7f4c7104 100644
--- a/lib/packages/service/dfx/models/payment/payment_info_error.dart
+++ b/lib/packages/service/dfx/models/payment/payment_info_error.dart
@@ -2,5 +2,6 @@ enum PaymentInfoError {
registrationRequired,
kycRequired,
minAmountNotMet,
+ bitboxDisconnected,
unknown,
}
diff --git a/lib/packages/service/dfx/real_unit_registration_service.dart b/lib/packages/service/dfx/real_unit_registration_service.dart
index 117301b5..2e331604 100644
--- a/lib/packages/service/dfx/real_unit_registration_service.dart
+++ b/lib/packages/service/dfx/real_unit_registration_service.dart
@@ -51,6 +51,9 @@ class RealUnitRegistrationService extends DFXAuthService {
/// registers a wallet and and adds the wallet to the new user
Future completeRegistration(Registration registration) async {
+ // EIP-712 registration signature requires the private key — promote the
+ // view-wallet (if any) to a fully unlocked SoftwareWallet first.
+ await appStore.ensureUnlocked();
final credentials = appStore.wallet.primaryAccount.primaryAddress;
// BitBox firmware rejects non-ASCII bytes in EIP-712 string fields.
// Transliterate everything that goes into the signed envelope AND the
diff --git a/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart b/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart
index fa512bf7..98a38195 100644
--- a/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart
+++ b/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart
@@ -78,6 +78,10 @@ class RealUnitSellPaymentInfoService extends DFXAuthService {
/// Confirms payment for Software Wallet
Future confirmPayment(SellPaymentInfo paymentInfo) async {
+ // EIP-712 + EIP-7702 typed-data signing requires the private key; promote
+ // the view-wallet to a fully unlocked SoftwareWallet before reading
+ // credentials.
+ await appStore.ensureUnlocked();
final credentials = appStore.wallet.currentAccount.primaryAddress;
_validateEip7702Data(paymentInfo.eip7702, credentials.address.hexEip55, paymentInfo.amount);
diff --git a/lib/packages/service/wallet_service.dart b/lib/packages/service/wallet_service.dart
index b2ab069e..875b9f72 100644
--- a/lib/packages/service/wallet_service.dart
+++ b/lib/packages/service/wallet_service.dart
@@ -13,8 +13,7 @@ class WalletService {
Future createSeedWallet(String name) async {
final mnemonic = bip39.generateMnemonic();
- final walletId = await _repository.createWallet(name, WalletType.software, mnemonic);
- return SoftwareWallet(walletId, name, mnemonic);
+ return _persistSoftwareWallet(name, mnemonic);
}
Future createBitboxWallet(String name) async {
@@ -25,9 +24,19 @@ class WalletService {
}
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);
+ final wallet = await _persistSoftwareWallet(name, seed);
+ await _settingsRepository.saveCurrentWalletId(wallet.id);
+ return wallet;
+ }
+
+ /// Builds the BIP32 wallet once to derive the public address, then persists
+ /// `(encryptedSeed, address)` so app-start can render the dashboard from the
+ /// cached address without re-running the derivation.
+ Future _persistSoftwareWallet(String name, String seed) async {
+ final fullWallet = SoftwareWallet(0, name, seed);
+ final address = fullWallet.currentAccount.primaryAddress.address.hexEip55;
+ final id = await _repository.createWallet(name, WalletType.software, seed, address);
+ return SoftwareWallet(id, name, seed);
}
Future createDebugWallet(String address) async {
@@ -36,19 +45,45 @@ class WalletService {
return DebugWallet(walletId, 'Debug', address);
}
+ /// Loads a wallet using only what's persisted in clear text — for software
+ /// wallets this means a [SoftwareViewWallet] (address only, no mnemonic in
+ /// memory). Use [unlockWalletById] when the private key is actually needed.
Future getWalletById(int id) async {
- final result = (await _repository.getWalletById(id))!;
- final walletType = WalletType.values[result.type];
+ final info = (await _repository.getWalletInfo(id))!;
+ final walletType = WalletType.values[info.type];
switch (walletType) {
case WalletType.software:
- return SoftwareWallet(result.id, result.name, result.seed);
+ // Legacy rows created before address-caching landed have an empty
+ // address column — decrypt the mnemonic this one time, persist the
+ // derived address back to the row, then keep using the fast path on
+ // subsequent loads.
+ if (info.address.isEmpty) {
+ final unlocked = (await _repository.getUnlockedWalletById(id))!;
+ final wallet = SoftwareWallet(unlocked.id, unlocked.name, unlocked.seed);
+ await _repository.updateAddress(
+ id,
+ wallet.currentAccount.primaryAddress.address.hexEip55,
+ );
+ return wallet;
+ }
+ return SoftwareViewWallet(info.id, info.name, info.address);
case WalletType.bitbox:
- return BitboxWallet(result.id, result.name, result.address, _bitboxService);
+ return BitboxWallet(info.id, info.name, info.address, _bitboxService);
case WalletType.debug:
- return DebugWallet(result.id, result.name, result.address);
+ return DebugWallet(info.id, info.name, info.address);
}
}
+ /// Decrypts the mnemonic and returns a [SoftwareWallet] ready to sign.
+ /// Throws if the wallet type is not software.
+ Future unlockWalletById(int id) async {
+ final info = (await _repository.getUnlockedWalletById(id))!;
+ if (WalletType.values[info.type] != WalletType.software) {
+ throw StateError('unlockWalletById called for non-software wallet');
+ }
+ return SoftwareWallet(info.id, info.name, info.seed);
+ }
+
Future setCurrentWallet(int walletId) async =>
await _settingsRepository.saveCurrentWalletId(walletId);
@@ -57,6 +92,11 @@ class WalletService {
return getWalletById(id);
}
+ Future unlockCurrentWallet() async {
+ final id = _settingsRepository.currentWalletId!;
+ return unlockWalletById(id);
+ }
+
Future deleteCurrentWallet() async {
final id = _settingsRepository.currentWalletId!;
await _repository.deleteWallet(id);
diff --git a/lib/packages/storage/secure_storage.dart b/lib/packages/storage/secure_storage.dart
index baef701f..9bb0f1f7 100644
--- a/lib/packages/storage/secure_storage.dart
+++ b/lib/packages/storage/secure_storage.dart
@@ -41,8 +41,16 @@ class SecureStorage {
return Uint8List.fromList(List.generate(16, (_) => random.nextInt(256)));
}
- static const _pinHashIterations = 600000;
- static const _legacyPinHashIterations = 10000;
+ // PIN-hash iteration count, picked for sub-second verification on mid-range
+ // phones. The PIN hash + salt live in [FlutterSecureStorage] (Android Keystore
+ // / iOS Keychain), so an offline brute-force first requires breaking that
+ // hardware-backed boundary. Online brute-force against the app UI is bounded
+ // by the lockout cascade in `verify_pin_cubit.dart`. The stronger guarantee
+ // for the actual private key comes from the OS-keystore-managed mnemonic
+ // encryption key — not from this hash. Earlier 10k / 600k hashes are still
+ // accepted and transparently upgraded to [_pinHashIterations].
+ static const _pinHashIterations = 100000;
+ static const _legacyIterationCandidates = [600000, 10000];
static String hashPin(String pin, Uint8List salt, {int iterations = _pinHashIterations}) {
final derivator = KeyDerivator('SHA-256/HMAC/PBKDF2');
@@ -51,9 +59,9 @@ class SecureStorage {
return bytesToHex(derivator.process(utf8.encode(pin)));
}
- /// Off-main-thread variant of [hashPin]. PBKDF2 with 600k iterations takes
- /// several seconds on a phone and freezes the UI when run synchronously, so
- /// any caller reachable from the UI should await this instead.
+ /// Off-main-thread variant of [hashPin]. Even at the reduced iteration count
+ /// PBKDF2 dominates the visible unlock latency, so any caller reachable from
+ /// the UI should await this instead of running it synchronously.
static Future hashPinAsync(
String pin,
Uint8List salt, {
@@ -90,11 +98,15 @@ class SecureStorage {
if (await hashPinAsync(pin, salt) == hash) return true;
- // Transparent rehash: verify against legacy iterations, upgrade if match
- if (await hashPinAsync(pin, salt, iterations: _legacyPinHashIterations) == hash) {
- final newHash = await hashPinAsync(pin, salt);
- await setPinHash(newHash);
- return true;
+ // Transparent rehash: any earlier iteration count we ever shipped is still
+ // accepted exactly once, then upgraded to the current target so subsequent
+ // unlocks pay the fast path.
+ for (final legacy in _legacyIterationCandidates) {
+ if (await hashPinAsync(pin, salt, iterations: legacy) == hash) {
+ final newHash = await hashPinAsync(pin, salt);
+ await setPinHash(newHash);
+ return true;
+ }
}
return false;
diff --git a/lib/packages/storage/wallet_storage.dart b/lib/packages/storage/wallet_storage.dart
index b348e0f8..87dc1f0c 100644
--- a/lib/packages/storage/wallet_storage.dart
+++ b/lib/packages/storage/wallet_storage.dart
@@ -9,6 +9,10 @@ extension WalletStorage on AppDatabase {
Future getWalletById(int id) =>
(select(walletInfos)..where((row) => row.id.equals(id))).getSingleOrNull();
+ Future updateWalletAddress(int id, String address) =>
+ (update(walletInfos)..where((row) => row.id.equals(id)))
+ .write(WalletInfosCompanion(address: Value(address)));
+
Future insertWalletAccount(int walletId, String name, int accountIndex) =>
into(walletAccountInfos).insert(
WalletAccountInfosCompanion.insert(
diff --git a/lib/packages/wallet/exceptions/wallet_locked_exception.dart b/lib/packages/wallet/exceptions/wallet_locked_exception.dart
new file mode 100644
index 00000000..180aec97
--- /dev/null
+++ b/lib/packages/wallet/exceptions/wallet_locked_exception.dart
@@ -0,0 +1,9 @@
+/// Thrown when a sign operation hits a [SoftwareViewWallet] — the mnemonic is
+/// still encrypted on disk and the caller must unlock the wallet (via
+/// `WalletService.unlockCurrentWallet`) before signing.
+class WalletLockedException implements Exception {
+ const WalletLockedException();
+
+ @override
+ String toString() => 'Wallet is locked';
+}
diff --git a/lib/packages/wallet/wallet.dart b/lib/packages/wallet/wallet.dart
index b83e3a08..1ff45cc7 100644
--- a/lib/packages/wallet/wallet.dart
+++ b/lib/packages/wallet/wallet.dart
@@ -3,6 +3,7 @@ import 'dart:typed_data';
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/exceptions/wallet_locked_exception.dart';
import 'package:realunit_wallet/packages/wallet/wallet_account.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
@@ -46,6 +47,54 @@ class SoftwareWallet extends AWallet {
void selectAccount(int index) => _currentAccount = WalletAccount(_bip32, index);
}
+/// Software wallet without the mnemonic in memory — only the public address is
+/// cached. Used at app startup so the dashboard renders before the (expensive
+/// and rarely needed) BIP32 derivation happens. Must be upgraded to a full
+/// [SoftwareWallet] via `WalletService.unlockCurrentWallet` before any sign
+/// operation.
+class SoftwareViewWallet extends AWallet {
+ @override
+ WalletType get walletType => WalletType.software;
+
+ @override
+ late final SoftwareViewWalletAccount primaryAccount;
+
+ late SoftwareViewWalletAccount _currentAccount;
+
+ @override
+ SoftwareViewWalletAccount get currentAccount => _currentAccount;
+
+ SoftwareViewWallet(super.id, super.name, String address) {
+ primaryAccount = SoftwareViewWalletAccount(0, _LockedCredentials(address));
+ _currentAccount = primaryAccount;
+ }
+}
+
+class _LockedCredentials extends CredentialsWithKnownAddress {
+ final EthereumAddress _address;
+
+ _LockedCredentials(String hexAddress) : _address = EthereumAddress.fromHex(hexAddress);
+
+ @override
+ EthereumAddress get address => _address;
+
+ @override
+ MsgSignature signToEcSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) =>
+ throw const WalletLockedException();
+
+ @override
+ Future signToSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) =>
+ throw const WalletLockedException();
+
+ @override
+ Future signPersonalMessage(Uint8List payload, {int? chainId}) =>
+ throw const WalletLockedException();
+
+ @override
+ Uint8List signPersonalMessageToUint8List(Uint8List payload, {int? chainId}) =>
+ throw const WalletLockedException();
+}
+
class BitboxWallet extends AWallet {
@override
WalletType get walletType => WalletType.bitbox;
diff --git a/lib/packages/wallet/wallet_account.dart b/lib/packages/wallet/wallet_account.dart
index c700a08c..ac7777b6 100644
--- a/lib/packages/wallet/wallet_account.dart
+++ b/lib/packages/wallet/wallet_account.dart
@@ -2,6 +2,7 @@ import 'dart:convert' show utf8;
import 'package:bip32/bip32.dart';
import 'package:convert/convert.dart';
+import 'package:realunit_wallet/packages/wallet/exceptions/wallet_locked_exception.dart';
import 'package:web3dart/web3dart.dart';
abstract class AWalletAccount {
@@ -39,3 +40,11 @@ class BitboxWalletAccount extends AWalletAccount {
Future signMessage(String message, {int addressIndex = 0}) async =>
'0x${hex.encode(await primaryAddress.signPersonalMessage(utf8.encode(message)))}';
}
+
+class SoftwareViewWalletAccount extends AWalletAccount {
+ SoftwareViewWalletAccount(super.accountIndex, super.primaryAddress);
+
+ @override
+ Future signMessage(String message, {int addressIndex = 0}) =>
+ throw const WalletLockedException();
+}
diff --git a/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart b/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart
index a598c04a..b65a2aed 100644
--- a/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart
+++ b/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart
@@ -4,6 +4,7 @@ import 'package:async/async.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_price_service.dart';
+import 'package:realunit_wallet/packages/service/dfx/exceptions/bitbox_exception.dart';
import 'package:realunit_wallet/packages/service/dfx/exceptions/payment/buy_exceptions.dart';
import 'package:realunit_wallet/packages/service/dfx/models/payment/buy/buy_payment_info.dart';
import 'package:realunit_wallet/packages/service/dfx/models/payment/payment_info_error.dart';
@@ -70,6 +71,8 @@ class BuyPaymentInfoCubit extends Cubit {
);
} on RegistrationRequiredException {
return const BuyPaymentInfoFailure(PaymentInfoError.registrationRequired);
+ } on BitboxNotConnectedException {
+ return const BuyPaymentInfoFailure(PaymentInfoError.bitboxDisconnected);
} catch (e) {
developer.log(e.toString());
return const BuyPaymentInfoFailure(PaymentInfoError.unknown);
diff --git a/lib/screens/buy/widgets/payment_additional_action_needed_button.dart b/lib/screens/buy/widgets/payment_additional_action_needed_button.dart
index 14d92049..9c2c21cf 100644
--- a/lib/screens/buy/widgets/payment_additional_action_needed_button.dart
+++ b/lib/screens/buy/widgets/payment_additional_action_needed_button.dart
@@ -5,6 +5,7 @@ import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/packages/service/dfx/models/payment/payment_info_error.dart';
import 'package:realunit_wallet/screens/buy/cubits/buy_converter/buy_converter_cubit.dart';
import 'package:realunit_wallet/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart';
+import 'package:realunit_wallet/screens/hardware_connect_bitbox/show_bitbox_reconnect_sheet.dart';
import 'package:realunit_wallet/setup/routing/routes/app_routes.dart';
import 'package:realunit_wallet/styles/colors.dart';
import 'package:realunit_wallet/widgets/buttons/app_filled_button.dart';
@@ -86,6 +87,23 @@ class PaymentAdditionalActionNeededButton extends StatelessWidget {
),
);
}
+ if (paymentState.error == PaymentInfoError.bitboxDisconnected) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 20),
+ child: AppFilledButton(
+ onPressed: () async {
+ final paymentInfoCubit = context.read();
+ final converterCubit = context.read();
+ await showBitboxReconnectSheet(context);
+ paymentInfoCubit.getPaymentInfo(
+ amount: amountController.text,
+ currency: converterCubit.state.currency,
+ );
+ },
+ label: S.of(context).bitboxReconnect,
+ ),
+ );
+ }
if (paymentState.error == PaymentInfoError.unknown) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
diff --git a/lib/screens/buy/widgets/payment_information.dart b/lib/screens/buy/widgets/payment_information.dart
index 3c6c4357..c3a9f1b5 100644
--- a/lib/screens/buy/widgets/payment_information.dart
+++ b/lib/screens/buy/widgets/payment_information.dart
@@ -38,6 +38,11 @@ class PaymentInformation extends StatelessWidget {
title: S.of(context).identityCheckRequired,
description: S.of(context).identityCheckDescription,
);
+ } else if (error == PaymentInfoError.bitboxDisconnected) {
+ return PaymentActionRequired(
+ title: S.of(context).bitboxDisconnectedTitle,
+ description: S.of(context).bitboxDisconnectedDescription,
+ );
} else if (error == PaymentInfoError.unknown) {
return PaymentActionRequired(
title: S.of(context).paymentInformationFailed,
diff --git a/lib/screens/create_wallet/bloc/create_wallet_cubit.dart b/lib/screens/create_wallet/bloc/create_wallet_cubit.dart
index 9610733a..65334e28 100644
--- a/lib/screens/create_wallet/bloc/create_wallet_cubit.dart
+++ b/lib/screens/create_wallet/bloc/create_wallet_cubit.dart
@@ -1,17 +1,33 @@
+import 'dart:developer' as developer;
+
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:realunit_wallet/packages/service/dfx/dfx_auth_service.dart';
import 'package:realunit_wallet/packages/service/wallet_service.dart';
import 'package:realunit_wallet/packages/wallet/wallet.dart';
part 'create_wallet_state.dart';
class CreateWalletCubit extends Cubit {
- CreateWalletCubit(this._service) : super(const CreateWalletState());
+ CreateWalletCubit(this._service, this._authService) : super(const CreateWalletState());
final WalletService _service;
+ final DFXAuthService _authService;
void createWallet() async {
final wallet = await _service.createSeedWallet('Obi-Wallet-Kenobi');
+ // Capture the DFX auth signature while the freshly generated mnemonic is
+ // still in memory — same rationale as the BitBox pairing flow. Non-fatal
+ // on failure; the lazy path in DFXAuthService.getSignature recovers later.
+ try {
+ await _authService.ensureSignatureFor(wallet.currentAccount);
+ } catch (e) {
+ developer.log(
+ 'initial signature capture failed: $e',
+ name: '$CreateWalletCubit',
+ );
+ }
+
emit(state.copyWith(wallet: wallet));
}
diff --git a/lib/screens/create_wallet/create_wallet_page.dart b/lib/screens/create_wallet/create_wallet_page.dart
index 5092dd0e..b7ff75be 100644
--- a/lib/screens/create_wallet/create_wallet_page.dart
+++ b/lib/screens/create_wallet/create_wallet_page.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart';
import 'package:realunit_wallet/packages/service/wallet_service.dart';
import 'package:realunit_wallet/screens/create_wallet/bloc/create_wallet_cubit.dart';
import 'package:realunit_wallet/screens/create_wallet/create_wallet_view.dart';
@@ -10,7 +11,12 @@ class CreateWalletPage extends StatelessWidget {
@override
Widget build(BuildContext context) => BlocProvider(
- create: (_) => CreateWalletCubit(getIt())..createWallet(),
+ create: (_) => CreateWalletCubit(
+ getIt(),
+ // DfxKycService is the smallest concrete DFXAuthService — only used here
+ // as a transport for ensureSignatureFor(account).
+ getIt(),
+ )..createWallet(),
child: const CreateWalletView(),
);
}
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 d7b46fe6..73044e04 100644
--- a/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart
+++ b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart
@@ -4,6 +4,7 @@ import 'dart:developer' as developer;
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/dfx/dfx_auth_service.dart';
import 'package:realunit_wallet/packages/service/wallet_service.dart';
import 'package:realunit_wallet/packages/utils/device_info.dart';
import 'package:realunit_wallet/packages/wallet/wallet.dart';
@@ -25,7 +26,8 @@ class ConnectBitboxCubit extends Cubit {
ConnectBitboxCubit(
this._service,
- this._walletService, {
+ this._walletService,
+ this._authService, {
Duration confirmPairingTimeout = _defaultConfirmPairingTimeout,
Duration createWalletTimeout = _defaultCreateWalletTimeout,
Duration pairingPinTimeout = _defaultPairingPinTimeout,
@@ -47,6 +49,7 @@ class ConnectBitboxCubit extends Cubit {
final BitboxService _service;
final WalletService _walletService;
+ final DFXAuthService _authService;
Timer? _checkForTimer;
Future? _pendingInit;
@@ -149,6 +152,19 @@ class ConnectBitboxCubit extends Cubit {
'${_createWalletTimeout.inSeconds}s. Try disconnecting and re-pairing.',
),
);
+ // Capture the DFX auth signature now, while the BitBox is guaranteed
+ // connected. Once persisted, every later buy / KYC / user-data call runs
+ // off the cached signature without needing the hardware wallet present.
+ // A failure here is non-fatal: we still complete pairing and rely on the
+ // lazy-create path in DFXAuthService.getSignature as a fallback.
+ try {
+ await _authService.ensureSignatureFor(wallet.currentAccount);
+ } catch (e) {
+ developer.log(
+ 'initial signature capture failed: $e',
+ name: '$ConnectBitboxCubit',
+ );
+ }
_service.startConnectionStatusObserver();
emit(BitboxConnected(wallet));
} catch (e) {
@@ -169,6 +185,9 @@ class ConnectBitboxCubit extends Cubit {
@override
Future close() {
_checkForTimer?.cancel();
+ // Detach from the in-flight init future so any late error doesn't surface
+ // as an unhandled exception after the cubit is gone.
+ _pendingInit?.ignore();
_pendingInit = null;
return super.close();
}
diff --git a/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart b/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart
index 45df05c7..54c329e6 100644
--- a/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart
+++ b/lib/screens/hardware_connect_bitbox/connect_bitbox_page.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:realunit_wallet/packages/hardware_wallet/bitbox.dart';
+import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart';
import 'package:realunit_wallet/packages/service/wallet_service.dart';
import 'package:realunit_wallet/packages/wallet/wallet.dart';
import 'package:realunit_wallet/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart';
@@ -17,6 +18,9 @@ class ConnectBitboxPage extends StatelessWidget {
create: (_) => ConnectBitboxCubit(
getIt(),
getIt(),
+ // DfxKycService is the smallest registered DFXAuthService — used only as
+ // a transport for ensureSignatureFor(account); no KYC-specific calls here.
+ getIt(),
),
child: ConnectBitboxView(onFinish: onFinish),
);
diff --git a/lib/screens/hardware_connect_bitbox/show_bitbox_reconnect_sheet.dart b/lib/screens/hardware_connect_bitbox/show_bitbox_reconnect_sheet.dart
new file mode 100644
index 00000000..b350636f
--- /dev/null
+++ b/lib/screens/hardware_connect_bitbox/show_bitbox_reconnect_sheet.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:realunit_wallet/screens/hardware_connect_bitbox/connect_bitbox_page.dart';
+import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart';
+
+/// Opens the BitBox pairing flow as a bottom sheet for the
+/// already-onboarded user, e.g. after the BitBox has been disconnected and a
+/// later action (buy info refresh, sell signing, user-data fetch) needs the
+/// device back.
+///
+/// Emits `SyncWalletServicesEvent` instead of `LoadWalletEvent` because the
+/// wallet itself is unchanged — only the underlying transport needs to be
+/// re-attached. Returns `true` if the user completed the re-pair.
+Future showBitboxReconnectSheet(BuildContext context) async {
+ final homeBloc = context.read();
+ final result = await showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ builder: (sheetContext) => ConnectBitboxPage(
+ onFinish: (wallet) {
+ homeBloc.add(SyncWalletServicesEvent(wallet));
+ Navigator.of(sheetContext).pop(true);
+ },
+ ),
+ );
+ return result == true;
+}
diff --git a/lib/screens/restore_wallet/cubit/restore_wallet/restore_wallet_cubit.dart b/lib/screens/restore_wallet/cubit/restore_wallet/restore_wallet_cubit.dart
index c1e0381f..27ee43fe 100644
--- a/lib/screens/restore_wallet/cubit/restore_wallet/restore_wallet_cubit.dart
+++ b/lib/screens/restore_wallet/cubit/restore_wallet/restore_wallet_cubit.dart
@@ -1,14 +1,19 @@
+import 'dart:developer' as developer;
+
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:realunit_wallet/packages/service/dfx/dfx_auth_service.dart';
import 'package:realunit_wallet/packages/service/wallet_service.dart';
import 'package:realunit_wallet/packages/wallet/wallet.dart';
part 'restore_wallet_state.dart';
class RestoreWalletCubit extends Cubit {
- RestoreWalletCubit(this._walletService) : super(const RestoreWalletState());
+ RestoreWalletCubit(this._walletService, this._authService)
+ : super(const RestoreWalletState());
final WalletService _walletService;
+ final DFXAuthService _authService;
void restoreWallet(String seed) async {
emit(const RestoreWalletState(isLoading: true));
@@ -17,6 +22,18 @@ class RestoreWalletCubit extends Cubit {
final wallet = await _walletService.restoreWallet('Obi-Wallet-Kenobi', normalizedSeed);
+ // Capture the DFX auth signature while the freshly restored mnemonic is
+ // still in memory — same rationale as the BitBox pairing flow. Non-fatal
+ // on failure; the lazy path in DFXAuthService.getSignature recovers later.
+ try {
+ await _authService.ensureSignatureFor(wallet.currentAccount);
+ } catch (e) {
+ developer.log(
+ 'initial signature capture failed: $e',
+ name: '$RestoreWalletCubit',
+ );
+ }
+
emit(
RestoreWalletState(
isLoading: false,
@@ -24,5 +41,4 @@ class RestoreWalletCubit extends Cubit {
),
);
}
-
}
diff --git a/lib/screens/restore_wallet/restore_wallet_page.dart b/lib/screens/restore_wallet/restore_wallet_page.dart
index e16903f7..6325b25d 100644
--- a/lib/screens/restore_wallet/restore_wallet_page.dart
+++ b/lib/screens/restore_wallet/restore_wallet_page.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart';
import 'package:realunit_wallet/packages/service/wallet_service.dart';
import 'package:realunit_wallet/screens/restore_wallet/cubit/restore_wallet/restore_wallet_cubit.dart';
import 'package:realunit_wallet/screens/restore_wallet/cubit/validate_seed/validate_seed_cubit.dart';
@@ -15,6 +16,9 @@ class RestoreWalletPage extends StatelessWidget {
BlocProvider(
create: (_) => RestoreWalletCubit(
getIt(),
+ // DfxKycService is the smallest concrete DFXAuthService — only used
+ // here as a transport for ensureSignatureFor(account).
+ getIt(),
),
),
BlocProvider(
diff --git a/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart b/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart
index 919115ad..00f6d3fc 100644
--- a/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart
+++ b/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart
@@ -4,6 +4,7 @@ import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:realunit_wallet/packages/service/app_store.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_price_service.dart';
+import 'package:realunit_wallet/packages/service/dfx/exceptions/bitbox_exception.dart';
import 'package:realunit_wallet/packages/service/dfx/exceptions/payment/buy_exceptions.dart';
import 'package:realunit_wallet/packages/service/dfx/models/payment/payment_info_error.dart';
import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/sell_payment_info.dart';
@@ -60,6 +61,13 @@ class SellPaymentInfoCubit extends Cubit {
message: e.toString(),
),
);
+ } on BitboxNotConnectedException catch (e) {
+ emit(
+ SellPaymentInfoFailure(
+ PaymentInfoError.bitboxDisconnected,
+ message: e.toString(),
+ ),
+ );
} catch (e) {
developer.log(e.toString());
emit(
diff --git a/lib/screens/sell_bitbox/cubit/sell_bitbox_cubit.dart b/lib/screens/sell_bitbox/cubit/sell_bitbox_cubit.dart
index 0d93fa68..c4c36785 100644
--- a/lib/screens/sell_bitbox/cubit/sell_bitbox_cubit.dart
+++ b/lib/screens/sell_bitbox/cubit/sell_bitbox_cubit.dart
@@ -8,6 +8,7 @@ import 'package:realunit_wallet/packages/hardware_wallet/bitbox_credentials.dart
import 'package:realunit_wallet/packages/service/app_store.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_blockchain_api_service.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_faucet_service.dart';
+import 'package:realunit_wallet/packages/service/dfx/exceptions/bitbox_exception.dart';
import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/dto/broadcast_transaction_request_dto.dart';
import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/sell_payment_info.dart';
import 'package:realunit_wallet/packages/service/dfx/real_unit_sell_payment_info_service.dart';
@@ -71,6 +72,9 @@ class SellBitboxCubit extends Cubit {
}
void _startEthPolling() {
+ // Cancel any prior timer first — retryAfterConnection can re-enter the
+ // faucet/polling branch while a previous timer is still alive.
+ _ethPollingTimer?.cancel();
_ethPollingTimer = Timer.periodic(const Duration(seconds: 5), (_) async {
try {
final balance = await _blockchainService.getEthBalance(_appStore.primaryAddress);
@@ -103,11 +107,17 @@ class SellBitboxCubit extends Cubit {
emit(SellBitboxError('BitBox wallet not connected'));
return;
}
+ if (!credentials.isConnected) {
+ emit(SellBitboxBitboxRequired());
+ return;
+ }
try {
emit(SellBitboxSwapping());
final signedSwap = await _signTransaction(swapState.rawSwapTransaction, credentials);
emit(SellBitboxAwaitingDepositConfirm(signedSwap, swapState.rawDepositTransaction));
+ } on BitboxNotConnectedException {
+ emit(SellBitboxBitboxRequired());
} catch (e) {
emit(SellBitboxError(e.toString()));
}
@@ -122,6 +132,10 @@ class SellBitboxCubit extends Cubit {
emit(SellBitboxError('BitBox wallet not connected'));
return;
}
+ if (!credentials.isConnected) {
+ emit(SellBitboxBitboxRequired());
+ return;
+ }
try {
emit(SellBitboxDepositing());
@@ -131,6 +145,8 @@ class SellBitboxCubit extends Cubit {
depositState.signedSwapTransaction,
);
await _broadcastDepositAndConfirm(depositState.signedSwapTransaction, signedDeposit);
+ } on BitboxNotConnectedException {
+ emit(SellBitboxBitboxRequired());
} catch (e) {
emit(SellBitboxError(e.toString()));
}
diff --git a/lib/screens/sell_bitbox/sell_bitbox_page.dart b/lib/screens/sell_bitbox/sell_bitbox_page.dart
index 1cc1cb9e..8a3effdc 100644
--- a/lib/screens/sell_bitbox/sell_bitbox_page.dart
+++ b/lib/screens/sell_bitbox/sell_bitbox_page.dart
@@ -7,8 +7,7 @@ import 'package:realunit_wallet/packages/service/dfx/dfx_blockchain_api_service.
import 'package:realunit_wallet/packages/service/dfx/dfx_faucet_service.dart';
import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/sell_payment_info.dart';
import 'package:realunit_wallet/packages/service/dfx/real_unit_sell_payment_info_service.dart';
-import 'package:realunit_wallet/screens/hardware_connect_bitbox/connect_bitbox_page.dart';
-import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart';
+import 'package:realunit_wallet/screens/hardware_connect_bitbox/show_bitbox_reconnect_sheet.dart';
import 'package:realunit_wallet/screens/sell_bitbox/cubit/sell_bitbox_cubit.dart';
import 'package:realunit_wallet/screens/sell_bitbox/widgets/sell_bitbox_deposit_step.dart';
import 'package:realunit_wallet/screens/sell_bitbox/widgets/sell_bitbox_eth_step.dart';
@@ -47,17 +46,8 @@ class SellBitboxView extends StatelessWidget {
return BlocConsumer(
listener: (context, state) async {
if (state is SellBitboxBitboxRequired) {
- final result = await showModalBottomSheet(
- context: context,
- isScrollControlled: true,
- builder: (_) => ConnectBitboxPage(
- onFinish: (wallet) {
- context.read().add(SyncWalletServicesEvent(wallet));
- context.pop(true);
- },
- ),
- );
- if (result == true && context.mounted) {
+ final reconnected = await showBitboxReconnectSheet(context);
+ if (reconnected && context.mounted) {
context.read().retryAfterConnection();
}
return;
diff --git a/lib/screens/settings_seed/bloc/settings_seed_cubit.dart b/lib/screens/settings_seed/bloc/settings_seed_cubit.dart
index f9f3ed6a..23467a95 100644
--- a/lib/screens/settings_seed/bloc/settings_seed_cubit.dart
+++ b/lib/screens/settings_seed/bloc/settings_seed_cubit.dart
@@ -1,13 +1,24 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:realunit_wallet/packages/service/app_store.dart';
import 'package:realunit_wallet/packages/wallet/wallet.dart';
part 'settings_seed_state.dart';
class SettingsSeedCubit extends Cubit {
- final SoftwareWallet wallet;
+ final AppStore _appStore;
- SettingsSeedCubit(this.wallet) : super(SettingsSeedState(wallet.seed));
+ SettingsSeedCubit(this._appStore) : super(const SettingsSeedState('')) {
+ _loadSeed();
+ }
+
+ Future _loadSeed() async {
+ // Revealing the recovery phrase needs the actual mnemonic in memory —
+ // promote a view-wallet to its unlocked form before reading the seed.
+ await _appStore.ensureUnlocked();
+ final wallet = _appStore.wallet as SoftwareWallet;
+ emit(SettingsSeedState(wallet.seed));
+ }
void toggleShowSeed() => emit(state.copyWith(showSeed: !state.showSeed));
}
diff --git a/lib/screens/settings_seed/settings_seed_page.dart b/lib/screens/settings_seed/settings_seed_page.dart
index ebd0ea6c..35ac7be7 100644
--- a/lib/screens/settings_seed/settings_seed_page.dart
+++ b/lib/screens/settings_seed/settings_seed_page.dart
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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:realunit_wallet/setup/di.dart';
@@ -11,7 +10,7 @@ class SettingsSeedPage extends StatelessWidget {
@override
Widget build(BuildContext context) => BlocProvider(
- create: (_) => SettingsSeedCubit((getIt().wallet as SoftwareWallet)),
+ create: (_) => SettingsSeedCubit(getIt()),
child: const SettingsSeedView(),
);
}
diff --git a/lib/screens/settings_user_data/cubit/settings_user_data_cubit.dart b/lib/screens/settings_user_data/cubit/settings_user_data_cubit.dart
index c8332766..36fc8c18 100644
--- a/lib/screens/settings_user_data/cubit/settings_user_data_cubit.dart
+++ b/lib/screens/settings_user_data/cubit/settings_user_data_cubit.dart
@@ -1,7 +1,10 @@
+import 'dart:developer' as developer;
+
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_country_service.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart';
+import 'package:realunit_wallet/packages/service/dfx/exceptions/bitbox_exception.dart';
import 'package:realunit_wallet/packages/service/dfx/models/kyc/dto/kyc_level_dto.dart';
import 'package:realunit_wallet/packages/service/dfx/models/kyc/kyc_level.dart';
import 'package:realunit_wallet/packages/service/dfx/models/registration/registration_user_type.dart';
@@ -90,8 +93,11 @@ class SettingsUserDataCubit extends Cubit {
pendingSteps: pendingSteps,
),
);
+ } on BitboxNotConnectedException {
+ emit(const SettingsUserDataBitboxDisconnected());
} catch (e) {
- emit(SettingsUserDataFailure(e.toString()));
+ developer.log(e.toString());
+ emit(const SettingsUserDataFailure());
}
}
}
diff --git a/lib/screens/settings_user_data/cubit/settings_user_data_state.dart b/lib/screens/settings_user_data/cubit/settings_user_data_state.dart
index a2e5d3b9..7c2d5514 100644
--- a/lib/screens/settings_user_data/cubit/settings_user_data_state.dart
+++ b/lib/screens/settings_user_data/cubit/settings_user_data_state.dart
@@ -16,12 +16,11 @@ class SettingsUserDataLoading extends SettingsUserDataState {
}
class SettingsUserDataFailure extends SettingsUserDataState {
- final String message;
-
- const SettingsUserDataFailure(this.message);
+ const SettingsUserDataFailure();
+}
- @override
- List