Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
7 changes: 7 additions & 0 deletions lib/screens/kyc/cubits/kyc/kyc_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ class KycCubit extends Cubit<KycState> {
),
);
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -78,12 +79,18 @@ class KycEmailVerificationCubit extends Cubit<KycEmailVerificationState> {
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',
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ class KycEmailVerificationRegistrationFailure
extends KycEmailVerificationState {
const KycEmailVerificationRegistrationFailure();
}

class KycEmailVerificationMergeProcessing extends KycEmailVerificationState {
const KycEmailVerificationMergeProcessing();
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -91,6 +92,8 @@ class KycEmailVerificationView extends StatelessWidget {
child: BlocBuilder<KycEmailVerificationCubit, KycEmailVerificationState>(
builder: (context, state) {
final isLoading = state is KycEmailVerificationLoading;
final isMergeProcessing =
state is KycEmailVerificationMergeProcessing;
final isBitbox = context
.read<HomeBloc>()
.state
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
}
22 changes: 22 additions & 0 deletions test/screens/kyc/cubits/kyc/kyc_cubit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,28 @@ void main() {
],
);

blocTest<KycCubit, KycState>(
'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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,32 @@ void main() {
},
);

blocTest<KycEmailVerificationCubit, KycEmailVerificationState>(
'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<KycEmailVerificationLoading>(),
isA<KycEmailVerificationMergeProcessing>(),
],
verify: (_) {
verifyNever(() => registrationService.registerWallet(any()));
},
);

blocTest<KycEmailVerificationCubit, KycEmailVerificationState>(
'registerWallet throws → RegistrationFailure, no Success '
'(failure is surfaced so the user can retry instead of proceeding '
Expand Down
Loading