Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ The transport is USB on Android and Bluetooth on iOS; the original BitBox 02 has
| Sell — DFX fiat off-ramp (IBAN) | always | mvp | widget (`sell/sell_page_test.dart`) + golden (`sell/sell_golden_test.dart`, `sell/sell_bank_account_selection_golden_test.dart`); added via [#321](https://github.com/RealUnitCH/app/pull/321) |
| KYC: Email + 2FA gate | always | mvp | widget (`kyc_email_page_test.dart`, `kyc_2fa_page_test.dart`) + golden (`kyc/kyc_email_golden_test.dart`, `kyc/kyc_email_verification_golden_test.dart`, `kyc/kyc_2fa_golden_test.dart`); cubit added via [#319](https://github.com/RealUnitCH/app/pull/319) |
| KYC: Registration + BitBox EIP-712 sign | always | mvp | widget (`kyc_registration_page_test.dart`) + golden (`kyc/kyc_registration_golden_test.dart`) + unit (`eip712_signer_test.dart`); cubit / `registration_submit` / sign-flow integration tests added via [#319](https://github.com/RealUnitCH/app/pull/319) + [#320](https://github.com/RealUnitCH/app/pull/320) |
| KYC: Personal data (correction after an ident data mismatch) | always | mvp | widget (`kyc_personal_data_page_test.dart`) + golden (`kyc/kyc_personal_data_golden_test.dart`) + cubit (`kyc_personal_data_cubit_test.dart`) |
| KYC: Nationality | always | mvp | widget (`kyc_nationality_page_test.dart`) + golden (`kyc/kyc_nationality_golden_test.dart`) |
| KYC: Financial data | always | mvp | widget (`kyc_financial_data_page_test.dart`) + golden (`kyc/kyc_financial_data_golden_test.dart`, `kyc/kyc_financial_data_failure_golden_test.dart`, `kyc/kyc_financial_data_loading_golden_test.dart`, `kyc/kyc_financial_data_questions_golden_test.dart`) |
| KYC: Ident | always | mvp | widget (`kyc_ident_page_test.dart`) + golden (`kyc/kyc_ident_golden_test.dart`) |
| KYC: Pending / Completed / Failure | always | mvp | widget (`kyc/subpages/kyc_*_page_test.dart`) + golden (`kyc/kyc_pending_golden_test.dart`, `kyc/kyc_completed_golden_test.dart`, `kyc/kyc_failure_golden_test.dart`, `kyc/kyc_loading_golden_test.dart`) |
| KYC: AccountMergeRequested / UnsupportedStepFailure | always | mvp | golden (`kyc/kyc_account_merge_golden_test.dart`); cubit paths added via [#319](https://github.com/RealUnitCH/app/pull/319) |
| KYC: AccountMergeRequested / UnsupportedStepFailure | always | mvp | widget (`kyc/subpages/kyc_unsupported_step_page_test.dart`) + golden (`kyc/kyc_account_merge_golden_test.dart`, `kyc/kyc_unsupported_step_golden_test.dart`); cubit paths added via [#319](https://github.com/RealUnitCH/app/pull/319) |
| `DFXAuthService` (lazy auth + 401 retry) | always | mvp | — (unit tests added via [#319](https://github.com/RealUnitCH/app/pull/319) + [#321](https://github.com/RealUnitCH/app/pull/321)) |
| `balance_service` (balance fetch + cache) | always | mvp | unit (`balance_service_test.dart`) |
| `format_fixed` / `parse_fixed` (decimal helpers) | always | mvp | unit (`format_fixed_test.dart`, `parse_fixed_test.dart`) |
Expand Down
5 changes: 4 additions & 1 deletion assets/languages/strings_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,12 @@
"kycMergeProcessingTitle": "Konten werden zusammengeführt",
"kycPending": "Daten werden geprüft",
"kycPendingDescription": "Ihr folgender Schritt ist gerade noch unter Prüfung: ${step}. Bitte haben Sie noch ein wenig Geduld und schauen Sie zu einem späteren Zeitpunkt nochmal rein.",
"kycPersonalDataDescription": "Bitte überprüfen Sie Ihre Angaben und korrigieren Sie alles, was nicht mit Ihrem Ausweisdokument übereinstimmt.",
"kycPersonalDataFailed": "Persönliche Daten konnten nicht gespeichert werden:\n${message}",
"kycSignatureUnsupportedDescription": "Dieses Feature erfordert eine EIP-712-Signatur. Im Debug-Modus (Adresse + Signatur) ist dies technisch nicht möglich. Bitte verwenden Sie eine Software-Wallet oder BitBox, um RealUnit zu nutzen.",
"kycSignatureUnsupportedTitle": "Signatur nicht verfügbar",
"kycUnsupportedStepDescription": "Der aktuelle KYC-Schritt (${step}) kann in dieser App nicht abgeschlossen werden. Bitte kontaktieren Sie den Support.",
"kycUnsupportedStepDescription": "Der nächste Schritt Ihrer Verifizierung kann in dieser App nicht abgeschlossen werden. Aktualisieren Sie, um zu prüfen, ob er weitergelaufen ist, oder wenden Sie sich an unser Support-Team, das dann übernimmt.",
"kycUnsupportedStepTitle": "Dieser Schritt wird anderswo fortgesetzt",
"label": "Bezeichnung",
"languageEnglish": "Englisch",
"languageGerman": "Deutsch",
Expand Down
5 changes: 4 additions & 1 deletion assets/languages/strings_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,12 @@
"kycMergeProcessingTitle": "Merging your accounts",
"kycPending": "Data is being verified",
"kycPendingDescription": "Your next step is currently being reviewed: ${step}. Please be patient and check back later.",
"kycPersonalDataDescription": "Please check your details and correct anything that does not match your identity document.",
"kycPersonalDataFailed": "Could not save your personal data:\n${message}",
"kycSignatureUnsupportedDescription": "This feature requires an EIP-712 signature. The Debug mode (address + signature) cannot produce one. Please use a Software Wallet or a BitBox to use RealUnit.",
"kycSignatureUnsupportedTitle": "Signature not available",
"kycUnsupportedStepDescription": "The current KYC step (${step}) cannot be completed in this app. Please contact support.",
"kycUnsupportedStepDescription": "The next step of your verification cannot be completed in this app. Refresh to check whether it has moved on, or contact our support team and they will take it from here.",
"kycUnsupportedStepTitle": "This step continues elsewhere",
"label": "Label",
"languageEnglish": "English",
"languageGerman": "German",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import 'package:realunit_wallet/packages/service/dfx/models/wallet/real_unit_reg
class RealUnitRegistrationInfoDto {
/// API-side routing decision for this wallet. Drives KYC dispatch in
/// `KycCubit._runCheckKyc` — see CONTRIBUTING.md "API as Decision
/// Authority". `userData` is populated for `addWallet` (prior payload)
/// and `newRegistration` (KYC pre-fill); `null` for `alreadyRegistered`
/// (no UX needed).
/// Authority". `userData` is populated whenever a registration row exists —
/// `alreadyRegistered` and `addWallet` both carry the stored signed payload —
/// and for `newRegistration` when DFX KYC data can pre-fill the form. It is
/// `null` only when the stored registration has no signed payload, or when
/// there is no verified personal data to pre-fill from.
///
/// `alreadyRegistered` is NOT a no-payload case: the personal-data KYC step
/// is reached only through that branch and seeds its correction form from
/// this payload, so treating it as null would dead-end that step.
final RealUnitRegistrationState state;
final RealUnitUserDataDto? realUnitUserDataDto;

Expand Down
6 changes: 4 additions & 2 deletions lib/screens/kyc/cubits/kyc/kyc_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ class KycCubit extends Cubit<KycState> {
emit(KycPending(step));
return;
case KycProcessStatus.inProgress:
await _continueKyc(generation);
await _continueKyc(generation, registrationInfo.realUnitUserDataDto);
return;
case KycProcessStatus.mergeProcessing:
// The user confirmed a merge and the backend is still processing it.
Expand Down Expand Up @@ -311,7 +311,7 @@ class KycCubit extends Cubit<KycState> {
}

/// should only be called after realunit registration was completed
Future<void> _continueKyc(int generation) async {
Future<void> _continueKyc(int generation, [RealUnitUserDataDto? realUnitUserData]) async {
final kycStatus = await _kycService.continueKyc(context: _kycContext);
if (isClosed || generation != _runGeneration) return;

Expand All @@ -338,12 +338,14 @@ class KycCubit extends Cubit<KycState> {
KycSuccess(
currentStep: kycStep,
urlOrToken: currentStep.session.url,
realUnitUserData: realUnitUserData,
),
);
}

KycStep? _mapStepName(KycStepName name) => switch (name) {
KycStepName.contactData => KycStep.registration,
KycStepName.personalData => KycStep.personalData,
KycStepName.nationalityData => KycStep.nationality,
KycStepName.ident => KycStep.ident,
KycStepName.financialData => KycStep.financialData,
Expand Down
5 changes: 4 additions & 1 deletion lib/screens/kyc/cubits/kyc/kyc_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ enum KycStep {
email,
confirmEmail,
registration,
personalData,
linkWallet,
legalDisclaimer,
nationality,
Expand Down Expand Up @@ -43,7 +44,9 @@ class KycSuccess extends KycState {

/// Server-side user record attached to the routing decision. Populated when
/// `RealUnitRegistrationService.getRegistrationInfo()` returns userData
/// alongside the state (`AddWallet` always, `NewRegistration` when the
/// alongside the state (`AddWallet` always, `AlreadyRegistered` whenever the
/// registration carries a signed payload — which is what the `personalData`
/// step seeds its correction form from — and `NewRegistration` when the
/// backend has fallback data). The cubit forwards the DTO so downstream
/// pages do not need to re-fetch — see CONTRIBUTING.md "Single round-trip
/// per decision".
Expand Down
12 changes: 7 additions & 5 deletions lib/screens/kyc/kyc_page_manager.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/packages/service/app_store.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart';
import 'package:realunit_wallet/packages/service/dfx/models/kyc/kyc_level.dart';
import 'package:realunit_wallet/packages/service/dfx/real_unit_legal_service.dart';
import 'package:realunit_wallet/packages/service/dfx/real_unit_registration_service.dart';
import 'package:realunit_wallet/screens/kyc/cubits/kyc/kyc_cubit.dart';
Expand All @@ -14,6 +12,7 @@ import 'package:realunit_wallet/screens/kyc/steps/financial_data/kyc_financial_d
import 'package:realunit_wallet/screens/kyc/steps/ident/kyc_ident_page.dart';
import 'package:realunit_wallet/screens/kyc/steps/link_wallet/kyc_link_wallet_page.dart';
import 'package:realunit_wallet/screens/kyc/steps/nationality/kyc_nationality_page.dart';
import 'package:realunit_wallet/screens/kyc/steps/personal_data/kyc_personal_data_page.dart';
import 'package:realunit_wallet/screens/kyc/steps/registration/kyc_registration_page.dart';
import 'package:realunit_wallet/screens/kyc/steps/signature_unsupported/kyc_signature_unsupported_page.dart';
import 'package:realunit_wallet/screens/kyc/subpages/kyc_account_merge_page.dart';
Expand All @@ -23,6 +22,7 @@ import 'package:realunit_wallet/screens/kyc/subpages/kyc_loading_page.dart';
import 'package:realunit_wallet/screens/kyc/subpages/kyc_manual_review_page.dart';
import 'package:realunit_wallet/screens/kyc/subpages/kyc_merge_processing_page.dart';
import 'package:realunit_wallet/screens/kyc/subpages/kyc_pending_page.dart';
import 'package:realunit_wallet/screens/kyc/subpages/kyc_unsupported_step_page.dart';
import 'package:realunit_wallet/screens/legal/legal_disclaimer_page.dart';
import 'package:realunit_wallet/setup/di.dart';

Expand Down Expand Up @@ -55,9 +55,7 @@ class KycViewManager extends StatelessWidget {
KycLoading() => const KycLoadingPage(),
KycFailure(:final message) => KycFailurePage(message: message),
KycSignatureUnsupportedFailure() => const KycSignatureUnsupportedPage(),
KycUnsupportedStepFailure(:final stepName) => KycFailurePage(
message: S.of(context).kycUnsupportedStepDescription(stepName?.value ?? '-'),
),
KycUnsupportedStepFailure() => const KycUnsupportedStepPage(),
KycAccountMergeRequested() => const KycAccountMergePage(),
KycMergeProcessing() => const KycMergeProcessingPage(),
KycManualReview() => const KycManualReviewPage(),
Expand All @@ -75,6 +73,10 @@ class KycViewManager extends StatelessWidget {
},
),
KycStep.registration => KycRegistrationPage(initialUserData: realUnitUserData),
KycStep.personalData => KycPersonalDataPage(
url: urlOrToken ?? '',
initialUserData: realUnitUserData,
),
KycStep.linkWallet => KycLinkWalletPage(userData: realUnitUserData),
KycStep.nationality => KycNationalityPage(url: urlOrToken ?? ''),
KycStep.twoFa => const Kyc2FaPage(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.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/kyc/kyc_personal_data.dart';

part 'kyc_personal_data_state.dart';

class KycPersonalDataCubit extends Cubit<KycPersonalDataState> {
final DfxKycService _kycService;

KycPersonalDataCubit(DfxKycService kycService)
: _kycService = kycService,
super(const KycPersonalDataInitial());

Future<void> submit({
required String url,
required KycAccountType accountType,
required String firstName,
required String lastName,
required String phone,
required String street,
required String houseNumber,
required String zip,
required String city,
required Country country,
}) async {
try {
emit(const KycPersonalDataLoading());
await _kycService.setData(
url,
KycPersonalData(
// Passed in, never assumed: the page refuses to render for anything but a personal
// account, and sending the same value it checked keeps the two from drifting apart.
accountType: accountType,
firstName: firstName,
lastName: lastName,
phone: phone,
address: KycAddress(
street: street,
// Always sent. The form requires it, and an omitted key would leave the stored value
// unchanged rather than clearing it — wrong for a form whose purpose is correction.
houseNumber: houseNumber,
zip: zip,
city: city,
country: country.id,
),
).toJson(),
);
emit(const KycPersonalDataSuccess());
} catch (e) {
emit(KycPersonalDataFailure(e.toString()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
part of 'kyc_personal_data_cubit.dart';

abstract class KycPersonalDataState extends Equatable {
const KycPersonalDataState();

@override
List<Object?> get props => [];
}

class KycPersonalDataInitial extends KycPersonalDataState {
const KycPersonalDataInitial();
}

class KycPersonalDataLoading extends KycPersonalDataState {
const KycPersonalDataLoading();
}

class KycPersonalDataSuccess extends KycPersonalDataState {
const KycPersonalDataSuccess();
}

class KycPersonalDataFailure extends KycPersonalDataState {
final String message;

const KycPersonalDataFailure(this.message);

@override
List<Object?> get props => [message];
}
Loading
Loading