You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
KycEmailVerificationCubit.checkEmailVerification (lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_cubit.dart:26-39) emits KycEmailVerificationSuccess unconditionally whenever the JWT account-id changes, even if the subsequent _completeRegistration call failed. Two distinct failure modes are silently swallowed: (a) realUnitUserDataDto returning null from getWalletStatus, and (b) registerWallet throwing. Both failures emit KycEmailVerificationRegistrationFailure first, but that state is immediately overwritten by the outer Success emit on the next line. The resulting symptom is a successful-looking merge confirmation with no EIP-712 registration signature actually produced, leaving the wallet in an inconsistent state.
This is documented by two tests in test/screens/kyc/steps/email/kyc_email_verification_cubit_test.dart (:131-148 and :150-170) which assert the current behaviour without questioning whether it is correct.
Code
// kyc_email_verification_cubit.dart:26-39Future<void> checkEmailVerification() async {
emit(constKycEmailVerificationLoading());
final currentAccountId =awaitgetAccountId();
_dfxService.invalidateAuthToken();
final newAccountId =awaitgetAccountId();
if (currentAccountId != newAccountId) {
await_completeRegistration();
emit(constKycEmailVerificationSuccess()); // ← unconditional
} else {
emit(constKycEmailVerificationFailure());
}
}
// kyc_email_verification_cubit.dart:41-50Future<void> _completeRegistration() async {
try {
final status =await _walletService.getWalletStatus();
if (status.realUnitUserDataDto ==null) throwException('No existing user data');
await _registrationService.registerWallet(status.realUnitUserDataDto!);
} catch (e) {
developer.log(e.toString());
emit(constKycEmailVerificationRegistrationFailure()); // ← overwritten by Success above
}
}
The RegistrationFailure snackbar in kyc_email_verification_page.dart:47-56 fires for an instant before Success triggers context.pop(true) (:57-59). On a real device the snackbar may not be perceptible.
Failure scenarios
Scenario 1 — realUnitUserDataDto == null
If getWalletStatus returns RealUnitWalletStatusDto(realUnitUserDataDto: null, ...) for a merged user, _completeRegistration throws "No existing user data" before registerWallet is even called. No EIP-712 sign is produced. The user is popped back as if everything succeeded.
Possible causes for null realUnitUserDataDto immediately after invalidateAuthToken:
Backend race: merge propagation in the user-data service lags the auth service, so getWalletStatus on the new account hits a window where the user data is still empty
Stale cache: the response is cached or the request returns before the backend has finished associating data
Genuine absence: the user has no DFX user-data record despite owning the account (edge case)
Verified by test kyc_email_verification_cubit_test.dart:131-148 titled 'changed account id but no userData → cubit settles on Success (RegistrationFailure is intermediate, overwritten by the outer Success emit)'.
Scenario 2 — registerWallet throws
If the backend rejects registerWallet (validation, signature mismatch, transient 5xx, network), the exception is caught and logged but otherwise swallowed.
Verified by test kyc_email_verification_cubit_test.dart:150-170 titled 'registerWallet throws: cubit still settles on Success (does not crash)'.
Server-side, the wallet is associated with the merged user account (JWT shows the new account) but the per-wallet EIP-712 registration signature was never written
Hit "wallet not registered" errors on the first sensitive operation downstream
Cause silent data divergence if they later fill out the registration form (because the form's submit calls completeRegistration, which produces a fresh signature with user-typed data — potentially overwriting authoritative DFX data with a transcription mistake)
#464 is the routing-level fix: mark _bitboxConfirmed = true in the email merge confirmation success path. That fix is correct for users whose _completeRegistration genuinely succeeded (no signature, no symptom).
This issue is the underlying robustness gap: even with #464 fixed, users who hit Scenario 1 or 2 will pass through silently and may end up in a degraded state without the app being able to tell. The two issues should be fixed together — fixing only #464 would mask the silent failure further.
Fix proposals
A. Stop swallowing failures (recommended)
In checkEmailVerification, only emit Success if _completeRegistration actually succeeded:
if (currentAccountId != newAccountId) {
final ok =await_completeRegistration();
if (ok) {
emit(constKycEmailVerificationSuccess());
}
// else: RegistrationFailure was already emitted from inside _completeRegistration
} else {
emit(constKycEmailVerificationFailure());
}
Change _completeRegistration to return a bool (or use a Result/Either type) and re-throw a typed exception caught by the caller. Update the two tests at kyc_email_verification_cubit_test.dart:131-148 and :150-170 to assert RegistrationFailure as the final state and adjust the page listener at kyc_email_verification_page.dart:47-56 accordingly so the snackbar is actually seen.
B. Retry getWalletStatus for the null-userData case
If Scenario 1 is caused by a backend race (auth service updates faster than user-data service), a bounded retry with exponential backoff might bridge the window without exposing the failure to the user. Combine with A so that genuine null data still surfaces as Failure.
C. Have the backend wait for full propagation before returning the new JWT
The cleanest fix is upstream: the merge endpoint should not return a new JWT until the user-data record is ready to be read by the user-data service. Out of scope for this repo but worth flagging to the API team.
References
lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_cubit.dart:26-50 — the cubit
lib/screens/kyc/steps/email/subpages/kyc_email_verification_page.dart:37-60 — the listener that consumes the states
test/screens/kyc/steps/email/kyc_email_verification_cubit_test.dart:131-170 — tests documenting current behaviour
Summary
KycEmailVerificationCubit.checkEmailVerification(lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_cubit.dart:26-39) emitsKycEmailVerificationSuccessunconditionally whenever the JWT account-id changes, even if the subsequent_completeRegistrationcall failed. Two distinct failure modes are silently swallowed: (a)realUnitUserDataDtoreturningnullfromgetWalletStatus, and (b)registerWalletthrowing. Both failures emitKycEmailVerificationRegistrationFailurefirst, but that state is immediately overwritten by the outerSuccessemit on the next line. The resulting symptom is a successful-looking merge confirmation with no EIP-712 registration signature actually produced, leaving the wallet in an inconsistent state.This is documented by two tests in
test/screens/kyc/steps/email/kyc_email_verification_cubit_test.dart(:131-148and:150-170) which assert the current behaviour without questioning whether it is correct.Code
The
RegistrationFailuresnackbar inkyc_email_verification_page.dart:47-56fires for an instant beforeSuccesstriggerscontext.pop(true)(:57-59). On a real device the snackbar may not be perceptible.Failure scenarios
Scenario 1 —
realUnitUserDataDto == nullIf
getWalletStatusreturnsRealUnitWalletStatusDto(realUnitUserDataDto: null, ...)for a merged user,_completeRegistrationthrows "No existing user data" beforeregisterWalletis even called. No EIP-712 sign is produced. The user is popped back as if everything succeeded.Possible causes for null
realUnitUserDataDtoimmediately afterinvalidateAuthToken:getWalletStatuson the new account hits a window where the user data is still emptyVerified by test
kyc_email_verification_cubit_test.dart:131-148titled'changed account id but no userData → cubit settles on Success (RegistrationFailure is intermediate, overwritten by the outer Success emit)'.Scenario 2 —
registerWalletthrowsIf the backend rejects
registerWallet(validation, signature mismatch, transient 5xx, network), the exception is caught and logged but otherwise swallowed.Verified by test
kyc_email_verification_cubit_test.dart:150-170titled'registerWallet throws: cubit still settles on Success (does not crash)'.Downstream impact
For any user who hits scenario 1 or 2:
KycCubit._bitboxConfirmedis not updated by the merge path (see Existing DFX customer is forced to re-enter personal data after successful merge #464), so the user is misrouted to the registration formcompleteRegistration, which produces a fresh signature with user-typed data — potentially overwriting authoritative DFX data with a transcription mistake)Relation to #464
#464 is the routing-level fix: mark
_bitboxConfirmed = truein the email merge confirmation success path. That fix is correct for users whose_completeRegistrationgenuinely succeeded (no signature, no symptom).This issue is the underlying robustness gap: even with #464 fixed, users who hit Scenario 1 or 2 will pass through silently and may end up in a degraded state without the app being able to tell. The two issues should be fixed together — fixing only #464 would mask the silent failure further.
Fix proposals
A. Stop swallowing failures (recommended)
In
checkEmailVerification, only emitSuccessif_completeRegistrationactually succeeded:Change
_completeRegistrationto return abool(or use a Result/Either type) and re-throw a typed exception caught by the caller. Update the two tests atkyc_email_verification_cubit_test.dart:131-148and:150-170to assertRegistrationFailureas the final state and adjust the page listener atkyc_email_verification_page.dart:47-56accordingly so the snackbar is actually seen.B. Retry
getWalletStatusfor the null-userData caseIf Scenario 1 is caused by a backend race (auth service updates faster than user-data service), a bounded retry with exponential backoff might bridge the window without exposing the failure to the user. Combine with A so that genuine null data still surfaces as Failure.
C. Have the backend wait for full propagation before returning the new JWT
The cleanest fix is upstream: the merge endpoint should not return a new JWT until the user-data record is ready to be read by the user-data service. Out of scope for this repo but worth flagging to the API team.
References
lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_cubit.dart:26-50— the cubitlib/screens/kyc/steps/email/subpages/kyc_email_verification_page.dart:37-60— the listener that consumes the statestest/screens/kyc/steps/email/kyc_email_verification_cubit_test.dart:131-170— tests documenting current behaviourStatus of this analysis
develop(HEAD59a2010)getWalletStatusimmediately afterinvalidateAuthTokenand the response ofregisterWalletwould be conclusiveEnvironment
develop(HEAD59a2010)