Skip to content
Merged
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
1 change: 0 additions & 1 deletion assets/languages/strings_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@
"registerEmailVerificationButton": "Ich habe meine E-Mail bestätigt",
"registerEmailVerificationDescription": "Wie es aussieht, haben Sie bereits ein Konto. Wir haben Ihnen gerade eine E-Mail geschickt. Um mit Ihrem bestehenden Konto fortzufahren, bestätigen Sie bitte Ihre E-Mail-Adresse, indem Sie auf den zugesandten Link klicken.",
"registerEmailVerificationFailed": "Sie haben Ihre E-Mail noch nicht bestätigt.",
"registerEmailVerificationRegistrationFailed": "Die Wallet-Registrierung wurde noch nicht abgeschlossen. Bitte versuchen Sie es in wenigen Sekunden erneut. Falls das Problem weiterhin besteht, kontaktieren Sie den Support.",
"registerEmailVerificationTitle": "Willkommen zurück!",
"registerPhoneNumberInvalid": "Telefonnummer ist erforderlich",
"registerPhoneNumberOnlyDigits": "Nur Zahlen sind erlaubt",
Expand Down
1 change: 0 additions & 1 deletion assets/languages/strings_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@
"registerEmailVerificationButton": "I have confirmed my email address",
"registerEmailVerificationDescription": "It looks like you already have an account. We have just sent you an email. To continue with your existing account, please confirm your email address by clicking on the link in the email.",
"registerEmailVerificationFailed": "You have not yet confirmed your email address.",
"registerEmailVerificationRegistrationFailed": "Wallet registration is not yet complete. Please try again in a few seconds. If the issue persists, contact support.",
"registerEmailVerificationTitle": "Welcome back!",
"registerPhoneNumberInvalid": "Phone number is required",
"registerPhoneNumberOnlyDigits": "Only numbers are allowed",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
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/real_unit_registration_service.dart';
import 'package:realunit_wallet/packages/utils/jwt_decoder.dart';

part 'kyc_email_verification_state.dart';

class KycEmailVerificationCubit extends Cubit<KycEmailVerificationState> {
final DFXAuthService _dfxService;
final RealUnitRegistrationService _registrationService;

// `Future.timeout` does not cancel the underlying work, so a late HTTP
// response from an earlier call can still resume after a retry. Each
Expand All @@ -21,84 +17,37 @@ class KycEmailVerificationCubit extends Cubit<KycEmailVerificationState> {
// sequences. Pattern mirrors `KycCubit._runGeneration`.
int _runGeneration = 0;

// Once the JWT account-id change has confirmed the merge, the auth side
// is settled — re-running the account-id comparison on a retry would just
// emit `Failure` ("email not yet confirmed") because `getAuthToken` keeps
// returning the new (merged) account. The remaining work that can still
// race is `getRegistrationInfo` propagation on the user-data side, so a
// retry after a `RegistrationFailure` should skip the auth-side check and
// go straight to `_completeRegistration`.
bool _mergeDetected = false;

KycEmailVerificationCubit({
required DFXAuthService dfxService,
required RealUnitRegistrationService registrationService,
}) : _dfxService = dfxService,
_registrationService = registrationService,
super(const KycEmailVerificationInitial());
KycEmailVerificationCubit({required DFXAuthService dfxService})
: _dfxService = dfxService,
super(const KycEmailVerificationInitial());

/// Confirms the account merge by detecting that the backend re-issued the JWT
/// for the merged (master) account. Registration is intentionally NOT done
/// here: once the merge is confirmed, the KYC flow (`KycCubit`) is the single
/// source of truth for what this wallet needs next — add wallet vs. full
/// registration — see CONTRIBUTING.md "API as Decision Authority". The page
/// pops on [KycEmailVerificationSuccess] and `KycEmailPage` re-runs `checkKyc`.
Future<void> checkEmailVerification() async {
final generation = ++_runGeneration;
if (isClosed) return;
emit(const KycEmailVerificationLoading());

if (!_mergeDetected) {
final currentAccountId = await getAccountId();
if (isClosed || generation != _runGeneration) return;
_dfxService.invalidateAuthToken();
final newAccountId = await getAccountId();
if (isClosed || generation != _runGeneration) return;

if (currentAccountId == newAccountId) {
// Email link not yet clicked, or token still cached. The user can
// retry by tapping again once the link in the confirmation mail has
// actually been visited.
emit(const KycEmailVerificationFailure());
return;
}
_mergeDetected = true;
final currentAccountId = await getAccountId();
if (isClosed || generation != _runGeneration) return;
_dfxService.invalidateAuthToken();
final newAccountId = await getAccountId();
if (isClosed || generation != _runGeneration) return;

if (currentAccountId == newAccountId) {
// Email link not yet clicked, or token still cached. The user can retry
// by tapping again once the link in the confirmation mail has been visited.
emit(const KycEmailVerificationFailure());
return;
}

// JWT account changed → backend recognised the merge. Now associate the
// new wallet with the merged user via the EIP-712 registration signature.
if (await _completeRegistration(generation)) {
if (isClosed || generation != _runGeneration) return;
emit(const KycEmailVerificationSuccess());
}
// else: _completeRegistration already emitted RegistrationFailure; we
// intentionally do NOT emit Success here so the verification page stays
// open and the user can retry without the failure being papered over.
}

/// Returns `true` when the wallet was successfully registered with the
/// (now-merged) user account. On failure the cubit is already in
/// [KycEmailVerificationRegistrationFailure] so the listener can show the
/// error to the user.
Future<bool> _completeRegistration(int generation) async {
try {
final info = await _registrationService.getRegistrationInfo();
if (isClosed || generation != _runGeneration) 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`.
developer.log(
'getRegistrationInfo returned null realUnitUserDataDto after merge',
);
emit(const KycEmailVerificationRegistrationFailure());
return false;
}
await _registrationService.registerWallet(info.realUnitUserDataDto!);
if (isClosed || generation != _runGeneration) return false;
return true;
} catch (e) {
if (isClosed || generation != _runGeneration) return false;
developer.log('registerWallet failed: $e');
emit(const KycEmailVerificationRegistrationFailure());
return false;
}
// JWT account changed → the backend recognised the merge. Hand back to the
// KYC flow, which routes the wallet to add-wallet or full registration.
emit(const KycEmailVerificationSuccess());
}

Future<int?> getAccountId() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,3 @@ class KycEmailVerificationSuccess extends KycEmailVerificationState {
class KycEmailVerificationFailure extends KycEmailVerificationState {
const KycEmailVerificationFailure();
}

class KycEmailVerificationRegistrationFailure
extends KycEmailVerificationState {
const KycEmailVerificationRegistrationFailure();
}
12 changes: 6 additions & 6 deletions lib/screens/kyc/steps/email/kyc_email_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ class _KycEmailFormState extends State<KycEmailForm> {
),
);
if (isConfirmed == true && context.mounted) {
// A successful merge confirmation already registered this
// wallet via `KycEmailVerificationCubit._completeRegistration`
// → `RealUnitRegistrationService.registerWallet`. The next
// `checkKyc()` re-fetches `getRegistrationInfo`, sees
// `AlreadyRegistered`, and routes forward — no local sign-gate
// flag needed.
// Merge confirmed. `checkKyc()` re-fetches `getRegistrationInfo`
// and routes this wallet by the API `state`: AddWallet → link
// wallet, NewRegistration → full registration form,
// AlreadyRegistered → forward. The KYC flow is the single source
// of registration routing — see CONTRIBUTING.md "API as Decision
// Authority".
context.read<KycCubit>().checkKyc();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:go_router/go_router.dart';
import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/packages/hardware_wallet/bitbox_credentials.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_widget_service.dart';
import 'package:realunit_wallet/packages/service/dfx/real_unit_registration_service.dart';
import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart';
import 'package:realunit_wallet/screens/kyc/steps/email/cubits/email_verification/kyc_email_verification_cubit.dart';
import 'package:realunit_wallet/setup/di.dart';
Expand All @@ -20,7 +19,6 @@ class KycEmailVerificationPage extends StatelessWidget {
return BlocProvider(
create: (context) => KycEmailVerificationCubit(
dfxService: getIt<DfxWidgetService>(),
registrationService: getIt<RealUnitRegistrationService>(),
),
child: const KycEmailVerificationView(),
);
Expand All @@ -42,16 +40,6 @@ class KycEmailVerificationView extends StatelessWidget {
),
);
}
if (state is KycEmailVerificationRegistrationFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
S.of(context).registerEmailVerificationRegistrationFailed,
),
backgroundColor: RealUnitColors.status.red600,
),
);
}
if (state is KycEmailVerificationSuccess) {
context.pop(true);
}
Expand Down
Loading
Loading