From bd9cf6ab94e22e12a37d61143ca854b1561265a9 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:34:50 +0200 Subject: [PATCH] Render merge-processing waiting state on RealUnit registration --- .../wallet/real_unit_registration_state.dart | 12 +++++-- lib/screens/kyc/cubits/kyc/kyc_cubit.dart | 7 ++++ .../kyc_email_verification_cubit.dart | 17 +++++++--- .../kyc_email_verification_state.dart | 4 +++ .../subpages/kyc_email_verification_page.dart | 12 +++++++ .../real_unit_registration_state_test.dart | 34 +++++++++++++++++++ .../kyc/cubits/kyc/kyc_cubit_test.dart | 22 ++++++++++++ .../kyc_email_verification_cubit_test.dart | 26 ++++++++++++++ 8 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 test/packages/service/dfx/models/wallet/real_unit_registration_state_test.dart diff --git a/lib/packages/service/dfx/models/wallet/real_unit_registration_state.dart b/lib/packages/service/dfx/models/wallet/real_unit_registration_state.dart index 98cd4bf0c..6edfdb393 100644 --- a/lib/packages/service/dfx/models/wallet/real_unit_registration_state.dart +++ b/lib/packages/service/dfx/models/wallet/real_unit_registration_state.dart @@ -13,15 +13,23 @@ enum RealUnitRegistrationState { /// No prior Aktionariat registration on this account. The app shows /// the full registration form, pre-filled from `userData` when present. - newRegistration(jsonName: 'NewRegistration'); + newRegistration(jsonName: 'NewRegistration'), + + /// An account merge for this user is still propagating on the backend, so + /// `userData` is not yet available. The app renders a waiting state and + /// re-checks instead of treating the absent `userData` as a failure. + mergeProcessing(jsonName: 'MergeProcessing'); final String jsonName; const RealUnitRegistrationState({required this.jsonName}); + /// Unknown values fall back to [mergeProcessing] (a benign waiting state the + /// user can re-check) so an additively-introduced backend state never crashes + /// the client — mirrors the tolerant parsing of other server-mirror enums. factory RealUnitRegistrationState.fromJson(String value) { return RealUnitRegistrationState.values.firstWhere( (e) => e.jsonName == value, - orElse: () => throw ArgumentError('Unknown RealUnitRegistrationState: $value'), + orElse: () => RealUnitRegistrationState.mergeProcessing, ); } } diff --git a/lib/screens/kyc/cubits/kyc/kyc_cubit.dart b/lib/screens/kyc/cubits/kyc/kyc_cubit.dart index 0401d3c2c..985ad4863 100644 --- a/lib/screens/kyc/cubits/kyc/kyc_cubit.dart +++ b/lib/screens/kyc/cubits/kyc/kyc_cubit.dart @@ -152,6 +152,13 @@ class KycCubit extends Cubit { ), ); return; + case RealUnitRegistrationState.mergeProcessing: + // A merge for this user is still propagating, so the registration + // data isn't ready. Render the waiting screen and let the user + // re-check instead of routing into a half-populated form — same + // signal the KYC processStatus path surfaces below. + emit(const KycMergeProcessing()); + return; } // Account-merge invitation is still surfaced from the step list because diff --git a/lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_cubit.dart b/lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_cubit.dart index 89dae281d..df253d266 100644 --- a/lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_cubit.dart +++ b/lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_cubit.dart @@ -3,6 +3,7 @@ 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/dfx/models/wallet/real_unit_registration_state.dart'; import 'package:realunit_wallet/packages/service/dfx/real_unit_registration_service.dart'; import 'package:realunit_wallet/packages/utils/jwt_decoder.dart'; @@ -78,12 +79,18 @@ class KycEmailVerificationCubit extends Cubit { try { final info = await _registrationService.getRegistrationInfo(); if (isClosed || generation != _runGeneration) return false; + if (info.state == RealUnitRegistrationState.mergeProcessing) { + // The merge is confirmed (JWT account already changed) but the backend + // is still re-parenting the merged-in data, so `userData` isn't ready + // yet. This is the expected post-merge propagation window — render a + // waiting state (not a failure) so the user re-checks instead of seeing + // a scary error. The retry skips the auth-side check via `_mergeDetected`. + emit(const KycEmailVerificationMergeProcessing()); + return false; + } if (info.realUnitUserDataDto == null) { - // Backend race: the auth service reports the merged account while the - // user-data service hasn't propagated yet. Surface as a recoverable - // failure so the user can retry by tapping the confirmation button - // again — by then propagation will usually have completed, and the - // retry path skips the auth-side check thanks to `_mergeDetected`. + // No merge in progress yet still no userData — a genuine failure the + // user should retry / report. developer.log( 'getRegistrationInfo returned null realUnitUserDataDto after merge', ); diff --git a/lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_state.dart b/lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_state.dart index 77cf44a90..f4c9b7710 100644 --- a/lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_state.dart +++ b/lib/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_state.dart @@ -27,3 +27,7 @@ class KycEmailVerificationRegistrationFailure extends KycEmailVerificationState { const KycEmailVerificationRegistrationFailure(); } + +class KycEmailVerificationMergeProcessing extends KycEmailVerificationState { + const KycEmailVerificationMergeProcessing(); +} diff --git a/lib/screens/kyc/steps/email/subpages/kyc_email_verification_page.dart b/lib/screens/kyc/steps/email/subpages/kyc_email_verification_page.dart index 9afb5b88d..ea895ccec 100644 --- a/lib/screens/kyc/steps/email/subpages/kyc_email_verification_page.dart +++ b/lib/screens/kyc/steps/email/subpages/kyc_email_verification_page.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; @@ -91,6 +92,8 @@ class KycEmailVerificationView extends StatelessWidget { child: BlocBuilder( builder: (context, state) { final isLoading = state is KycEmailVerificationLoading; + final isMergeProcessing = + state is KycEmailVerificationMergeProcessing; final isBitbox = context .read() .state @@ -100,6 +103,15 @@ class KycEmailVerificationView extends StatelessWidget { return Column( spacing: 12, children: [ + if (isMergeProcessing) ...[ + const CupertinoActivityIndicator(), + Text( + S.of(context).kycMergeProcessingDescription, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: RealUnitColors.neutral500), + ), + ], AppFilledButton( state: isLoading ? .loading : .idle, onPressed: () => context diff --git a/test/packages/service/dfx/models/wallet/real_unit_registration_state_test.dart b/test/packages/service/dfx/models/wallet/real_unit_registration_state_test.dart new file mode 100644 index 000000000..e9cd5640d --- /dev/null +++ b/test/packages/service/dfx/models/wallet/real_unit_registration_state_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:realunit_wallet/packages/service/dfx/models/wallet/real_unit_registration_state.dart'; + +void main() { + group('RealUnitRegistrationState.fromJson', () { + test('parses all known server values', () { + expect( + RealUnitRegistrationState.fromJson('AlreadyRegistered'), + RealUnitRegistrationState.alreadyRegistered, + ); + expect( + RealUnitRegistrationState.fromJson('AddWallet'), + RealUnitRegistrationState.addWallet, + ); + expect( + RealUnitRegistrationState.fromJson('NewRegistration'), + RealUnitRegistrationState.newRegistration, + ); + expect( + RealUnitRegistrationState.fromJson('MergeProcessing'), + RealUnitRegistrationState.mergeProcessing, + ); + }); + + test('falls back to mergeProcessing for an unknown (additively introduced) value', () { + // A future backend state must not crash the client — it degrades to a + // benign waiting state the user can re-check. + expect( + RealUnitRegistrationState.fromJson('SomethingNewFromBackend'), + RealUnitRegistrationState.mergeProcessing, + ); + }); + }); +} diff --git a/test/screens/kyc/cubits/kyc/kyc_cubit_test.dart b/test/screens/kyc/cubits/kyc/kyc_cubit_test.dart index e0d865854..6c8c62b3e 100644 --- a/test/screens/kyc/cubits/kyc/kyc_cubit_test.dart +++ b/test/screens/kyc/cubits/kyc/kyc_cubit_test.dart @@ -279,6 +279,28 @@ void main() { ], ); + blocTest( + 'emits KycMergeProcessing when wallet status reports MergeProcessing', + setUp: () { + when(() => kycService.getKycStatus()).thenAnswer( + (_) async => _kycStatus(level: KycLevel.level20), + ); + when(() => kycService.getUser()).thenAnswer((_) async => _user()); + when(() => registrationService.getRegistrationInfo()).thenAnswer( + (_) async => _walletStatus(RealUnitRegistrationState.mergeProcessing), + ); + }, + build: buildCubit, + act: (cubit) async { + cubit.markLegalDisclaimerAccepted(); + await cubit.checkKyc(); + }, + expect: () => [ + const KycLoading(), + const KycMergeProcessing(), + ], + ); + // Wallet-mode signing-capability gate: the address+signature debug // wallet cannot produce an EIP-712 signature. The cubit must surface // `KycSignatureUnsupportedFailure` BEFORE emitting `KycSuccess` for any diff --git a/test/screens/kyc/steps/email/kyc_email_verification_cubit_test.dart b/test/screens/kyc/steps/email/kyc_email_verification_cubit_test.dart index 588a801a7..e834c8611 100644 --- a/test/screens/kyc/steps/email/kyc_email_verification_cubit_test.dart +++ b/test/screens/kyc/steps/email/kyc_email_verification_cubit_test.dart @@ -148,6 +148,32 @@ void main() { }, ); + blocTest( + 'changed account id + state=mergeProcessing → MergeProcessing, no ' + 'RegistrationFailure, no registerWallet (post-merge propagation window ' + 'renders a waiting state instead of an error)', + setUp: () { + final tokens = [_fakeJwt(1), _fakeJwt(2)]; + var i = 0; + when(() => auth.getAuthToken()).thenAnswer((_) async => tokens[i++]); + when(() => registrationService.getRegistrationInfo()).thenAnswer( + (_) async => RealUnitRegistrationInfoDto( + state: RealUnitRegistrationState.mergeProcessing, + realUnitUserDataDto: null, + ), + ); + }, + build: build, + act: (c) => c.checkEmailVerification(), + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verifyNever(() => registrationService.registerWallet(any())); + }, + ); + blocTest( 'registerWallet throws → RegistrationFailure, no Success ' '(failure is surfaced so the user can retry instead of proceeding '