diff --git a/assets/languages/strings_de.arb b/assets/languages/strings_de.arb index 6244e58d..9e05b096 100644 --- a/assets/languages/strings_de.arb +++ b/assets/languages/strings_de.arb @@ -204,6 +204,7 @@ "registerPhoneNumberInvalid": "Telefonnummer ist erforderlich", "registerPhoneNumberOnlyDigits": "Nur Zahlen sind erlaubt", "registerPhoneNumberTooShort": "Telefonnummer ist zu kurz", + "registrationFailed": "Registrierung fehlgeschlagen:\n${message}", "registrationRequired": "Registrierung erforderlich", "registrationRequiredDescription": "Um RealUnit Token kaufen zu können, müssen Sie sich einmalig registrieren.", "reset": "Zurücksetzen", @@ -254,6 +255,7 @@ "settingsWalletBackupSubtitle2": "Dies ist die einzige Möglichkeit, Ihre Wallet wiederherzustellen.", "showSeed": "Seed anzeigen", "signature": "Signatur", + "signingCancelled": "Signatur abgebrochen — bitte BitBox erneut bestätigen", "signMessage": "Signierte Nachricht", "signMessageGet": "Signierte Nachricht abrufen", "skip": "Überspringen", diff --git a/assets/languages/strings_en.arb b/assets/languages/strings_en.arb index f0f12ad2..13c645b1 100644 --- a/assets/languages/strings_en.arb +++ b/assets/languages/strings_en.arb @@ -204,6 +204,7 @@ "registerPhoneNumberInvalid": "Phone number is required", "registerPhoneNumberOnlyDigits": "Only numbers are allowed", "registerPhoneNumberTooShort": "Phone number is too short", + "registrationFailed": "Registration failed:\n${message}", "registrationRequired": "Registration required", "registrationRequiredDescription": "To purchase RealUnit tokens, you must register once.", "reset": "Reset", @@ -254,6 +255,7 @@ "settingsWalletBackupSubtitle2": "This is the only way to recover your wallet.", "showSeed": "Show Seed", "signature": "Signature", + "signingCancelled": "Signature cancelled — please confirm on the BitBox again", "signMessage": "Sign Message", "signMessageGet": "Get Sign Message", "skip": "Skip", diff --git a/lib/packages/service/dfx/dfx_auth_service.dart b/lib/packages/service/dfx/dfx_auth_service.dart index 4c6f8924..00be0445 100644 --- a/lib/packages/service/dfx/dfx_auth_service.dart +++ b/lib/packages/service/dfx/dfx_auth_service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:realunit_wallet/packages/config/api_config.dart'; import 'package:realunit_wallet/packages/service/app_store.dart'; +import 'package:realunit_wallet/packages/wallet/exceptions/signing_cancelled_exception.dart'; import 'package:realunit_wallet/packages/wallet/wallet_account.dart'; abstract class DFXAuthService { @@ -48,7 +49,7 @@ abstract class DFXAuthService { final signature = await wallet.signMessage(message).timeout(_signMessageTimeout); if (signature.isEmpty || signature == '0x') { - throw Exception('Wallet returned an empty signature'); + throw const SigningCancelledException(); } await appStore.sessionCache.saveSignature(walletAddress, signature); diff --git a/lib/packages/wallet/eip712_signer.dart b/lib/packages/wallet/eip712_signer.dart index 944317c1..f19f0e78 100644 --- a/lib/packages/wallet/eip712_signer.dart +++ b/lib/packages/wallet/eip712_signer.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:eth_sig_util_plus/eth_sig_util_plus.dart'; import 'package:realunit_wallet/packages/hardware_wallet/bitbox_credentials.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/dto/eip7702/eip7702_data_dto.dart'; +import 'package:realunit_wallet/packages/wallet/exceptions/signing_cancelled_exception.dart'; import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; @@ -126,7 +127,7 @@ class Eip712Signer { // guard the empty sig would be sent to the backend and the abort would // be misread as a successful sign. if (signature.isEmpty || signature == '0x') { - throw Exception('Signature was empty — wallet may have been cancelled or disconnected'); + throw const SigningCancelledException(); } return signature; } diff --git a/lib/packages/wallet/exceptions/signing_cancelled_exception.dart b/lib/packages/wallet/exceptions/signing_cancelled_exception.dart new file mode 100644 index 00000000..4d2c5b00 --- /dev/null +++ b/lib/packages/wallet/exceptions/signing_cancelled_exception.dart @@ -0,0 +1,6 @@ +class SigningCancelledException implements Exception { + const SigningCancelledException(); + + @override + String toString() => 'SigningCancelledException'; +} diff --git a/lib/screens/kyc/steps/registration/cubits/registration_submit/kyc_registration_submit_cubit.dart b/lib/screens/kyc/steps/registration/cubits/registration_submit/kyc_registration_submit_cubit.dart index 1beaec85..00fbbbe2 100644 --- a/lib/screens/kyc/steps/registration/cubits/registration_submit/kyc_registration_submit_cubit.dart +++ b/lib/screens/kyc/steps/registration/cubits/registration_submit/kyc_registration_submit_cubit.dart @@ -66,7 +66,7 @@ class KycRegistrationSubmitCubit extends Cubit { } } catch (e) { developer.log(e.toString()); - emit(KycRegistrationSubmitFailure(e.toString())); + emit(KycRegistrationSubmitFailure(e.toString(), cause: e)); return; } } @@ -92,7 +92,7 @@ class KycRegistrationSubmitCubit extends Cubit { emit(const KycRegistrationSubmitSuccess(RegistrationStatus.completed)); } catch (e) { developer.log(e.toString()); - emit(KycRegistrationSubmitFailure(e.toString())); + emit(KycRegistrationSubmitFailure(e.toString(), cause: e)); } } diff --git a/lib/screens/kyc/steps/registration/cubits/registration_submit/kyc_registration_submit_state.dart b/lib/screens/kyc/steps/registration/cubits/registration_submit/kyc_registration_submit_state.dart index 07abe338..4cfa1a44 100644 --- a/lib/screens/kyc/steps/registration/cubits/registration_submit/kyc_registration_submit_state.dart +++ b/lib/screens/kyc/steps/registration/cubits/registration_submit/kyc_registration_submit_state.dart @@ -22,11 +22,12 @@ class KycRegistrationSubmitSuccess extends KycRegistrationSubmitState { class KycRegistrationSubmitFailure extends KycRegistrationSubmitState { final String message; + final Object? cause; - const KycRegistrationSubmitFailure(this.message); + const KycRegistrationSubmitFailure(this.message, {this.cause}); @override - List get props => [message]; + List get props => [message, cause]; } class KycRegistrationSubmitBitboxRequired extends KycRegistrationSubmitState { diff --git a/lib/screens/kyc/steps/registration/kyc_registration_page.dart b/lib/screens/kyc/steps/registration/kyc_registration_page.dart index c9a2e627..8036a5c1 100644 --- a/lib/screens/kyc/steps/registration/kyc_registration_page.dart +++ b/lib/screens/kyc/steps/registration/kyc_registration_page.dart @@ -4,11 +4,13 @@ import 'package:flutter/cupertino.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/packages/service/dfx/dfx_kyc_service.dart'; import 'package:realunit_wallet/packages/service/dfx/models/country/country.dart'; import 'package:realunit_wallet/packages/service/dfx/models/registration/registration_status.dart'; import 'package:realunit_wallet/packages/service/dfx/models/registration/registration_user_type.dart'; import 'package:realunit_wallet/packages/service/dfx/real_unit_registration_service.dart'; +import 'package:realunit_wallet/packages/wallet/exceptions/signing_cancelled_exception.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/kyc/cubits/kyc/kyc_cubit.dart'; @@ -109,9 +111,12 @@ class _KycRegistrationViewState extends State { } } if (state is KycRegistrationSubmitFailure) { + final message = state.cause is SigningCancelledException + ? S.of(context).signingCancelled + : S.of(context).registrationFailed(state.message); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Registration failed:\n${state.message}'), + content: Text(message), backgroundColor: RealUnitColors.status.red600, ), ); diff --git a/test/packages/wallet/eip712_signer_test.dart b/test/packages/wallet/eip712_signer_test.dart index ae968cb9..45e8f9da 100644 --- a/test/packages/wallet/eip712_signer_test.dart +++ b/test/packages/wallet/eip712_signer_test.dart @@ -1,10 +1,32 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:realunit_wallet/packages/hardware_wallet/bitbox_credentials.dart'; import 'package:realunit_wallet/packages/service/dfx/models/country/country.dart'; import 'package:realunit_wallet/packages/service/dfx/models/registration/registration.dart'; import 'package:realunit_wallet/packages/service/dfx/models/registration/registration_user_type.dart'; import 'package:realunit_wallet/packages/wallet/eip712_signer.dart'; +import 'package:realunit_wallet/packages/wallet/exceptions/signing_cancelled_exception.dart'; import 'package:web3dart/web3dart.dart'; +class _MockBitboxCredentials extends Mock implements BitboxCredentials {} + +Future _signWith(BitboxCredentials credentials) => Eip712Signer.signRegistration( + credentials: credentials, + chainId: 1, + type: RegistrationUserType.human.jsonName, + email: 'cancel@dfx.swiss', + name: 'Cancel User', + phoneNumber: '+41790000000', + birthday: '1990-01-01', + nationality: 'CH', + addressStreet: 'Teststrasse 1', + addressPostalCode: '8000', + addressCity: 'Zurich', + addressCountry: 'CH', + swissTaxResidence: true, + registrationDate: '2026-05-12', +); + void main() { late String privateKeyHex; late RegistrationUserType type; @@ -79,5 +101,19 @@ void main() { '0xa11cb57186b9c9f9a09fafa7a3aa256ab14ca030d7eba89f35026b64925d617b3e2cb15349ca561fae5e431deed3f1aa69c7d391cfba80aa6111e753fa782ea21c', ); }); + + for (final emptySignature in const ['', '0x']) { + test('throws SigningCancelledException when BitBox returns "$emptySignature"', () async { + final credentials = _MockBitboxCredentials(); + when( + () => credentials.address, + ).thenReturn(EthereumAddress.fromHex('0x0000000000000000000000000000000000000001')); + when( + () => credentials.signTypedDataV4(any(), any()), + ).thenAnswer((_) async => emptySignature); + + expect(() => _signWith(credentials), throwsA(isA())); + }); + } }); }