Skip to content

feat(realunit): pre-fill registration form from wallet status#600

Merged
TaprootFreak merged 5 commits into
developfrom
feat/prefill-registration-from-wallet-status
May 28, 2026
Merged

feat(realunit): pre-fill registration form from wallet status#600
TaprootFreak merged 5 commits into
developfrom
feat/prefill-registration-from-wallet-status

Conversation

@TaprootFreak
Copy link
Copy Markdown
Contributor

Summary

Pre-fills the RealUnit registration wizard with the values the user has already verified through DFX KYC, replacing the current empty-controllers UX. Pairs with DFXswiss/api#3780.

Why

Today every user reaching the buy screen with completed DFX KYC gets sent into the Aktionariat registration wizard with all fields blank. The backend's `completeRegistration` runs `isPersonalDataMatching` over every personal-data field after transliteration — name, phone, full address, country — so any guess-and-type discrepancy hard-fails the submit. The user is forced to reproduce their KYC submission byte-for-byte from memory, which is not realistic for street formatting, house-number layout, or city spelling. The companion API PR exposes the existing record on `GET /v1/realunit/wallet/status`; this PR consumes it.

What changed

`lib/screens/kyc/steps/registration/kyc_registration_page.dart`

  • `KycRegistrationView` becomes prefill-aware: on `initState`, calls `RealUnitWalletService.getWalletStatus()` and, if a `realUnitUserDataDto` is present, resolves the nationality and address `Country` objects via `DfxCountryService.getCountryBySymbol(...)` and then initializes every controller in one `setState` pass
  • Body conditionally renders a `CupertinoActivityIndicator` while `_prefillLoading` is true so the controllers are populated before any form field's `initState` reads them (`PhoneNumberField` / `BirthdayField` snapshot their controllers eagerly)
  • Failure path degrades gracefully — the form opens empty, matching pre-PR behaviour for unverified accounts
  • Passes the resolved Countries down to the step widgets via `initialNationality` / `initialCountry`

`lib/widgets/form/country_field.dart`

  • New optional `initialValue: Country?` parameter forwarded into the underlying `DropdownField`
  • `DropdownButtonFormField` does not fire `onChanged` for `initialValue`, so the field also pushes the preselected country back to the parent's `ValueNotifier` via a one-shot post-frame callback after countries load — keeps display and controller in sync without a second tap
  • Falls back to no preselection when the initial value is not in the filtered country list (e.g. residence purpose hiding a non-KYC-allowed country)

`lib/screens/kyc/steps/registration/steps/kyc_registration_{personal,address}_step.dart`

  • Add optional `initialNationality` / `initialCountry` parameters; pass them straight to `CountryField.initialValue`

Tests

  • `test/widgets/form/country_field_test.dart` — two new cases: `initialValue` preselects + fires `onChanged` once; `initialValue` absent from filtered list is ignored
  • `test/helper/golden_mocks.dart` — new `MockRealUnitWalletService` for golden and widget tests
  • `test/screens/kyc/steps/kyc_registration_page_test.dart` — DI block registers the wallet-service mock returning `isRegistered: false, userData: null` (no-prefill path); existing "renders ..." tests gain a `pumpAndSettle()` so the prefill loading state clears before the form is asserted on
  • `test/goldens/screens/kyc/kyc_registration_golden_test.dart` — registers the same wallet-service mock at `setUpAll` so the golden builds without throwing on the uninitialised `getIt()` lookup

Out of scope

The deeper redesign — driving every onboarding step purely from API-shaped definitions (`show`/`skip` per step, schema-driven forms, signature-only confirmation when everything is known) — is being scoped separately. This PR is the minimal change that converts the current re-typing trap into a one-tap confirmation flow for KYC-completed users.

Manual verification

Tested against PRD with userData 290795 (Cyrill, KYC 50, three wallets): all form fields land in the wizard pre-populated and the submit path becomes "continue → continue → sign" without any user edits required. Failure path verified by killing the wallet-status request — form opens empty as before.

Pair PR

DFXswiss/api#3780 — backend mapper that exposes the user_data fallback. App PR is safe to land first (no `userData` in the response = empty form, same as today), but the UX gain materialises only once the API change ships to dev.

Pair with the API's wallet/status user_data fallback
(DFXswiss/api#3780). When the user lands on the RealUnit
registration screen with completed DFX KYC, the form now starts
pre-populated from the values the backend already holds, instead
of forcing the user to retype every field and hoping each one
matches the stored record byte-for-byte (which `completeRegistration`
enforces server-side via `isPersonalDataMatching`).

`KycRegistrationView` fetches `getWalletStatus()` in initState,
resolves the nationality and address Country objects by symbol,
and initializes the controllers in one setState pass. The form
area renders a CupertinoActivityIndicator while the fetch is in
flight. Any failure falls through to the existing empty-form
behaviour — degraded gracefully, no regression for unverified
accounts.

`CountryField` learns an optional `initialValue: Country?` that
gets passed through to the underlying dropdown. Because
`DropdownButtonFormField` does not invoke its `onChanged` callback
for `initialValue`, the field also propagates the preselected
country to the parent controller once via a post-frame callback
so the submit step sees the same value the user sees.

Tests gain the new `RealUnitWalletService` mock (golden + unit
fixtures), two `CountryField.initialValue` cases (preselect +
propagate, ignore-when-filtered), and the existing render tests
now `pumpAndSettle` to clear the prefill loading state before
asserting on the form widgets.
Replace the local `_registrationSignProduced` session flag with the new
`state` field on `RealUnitWalletStatusDto` returned by
`GET /v1/realunit/wallet/status` (paired API change DFXswiss/api#3782).
The API is now the single source of truth for whether the user needs a
full registration form, a one-tap "Add wallet" confirmation, can skip
the registration step entirely, or is in an edge-case KYC-required
state. KycCubit re-fetches wallet status after every successful
registration round-trip and dispatches from whatever the backend now
reports — no client-side flag to keep in sync.

Four-state dispatch in `KycCubit._runCheckKyc` (after the disclaimer gate):
- NewRegistration  → KycSuccess(KycStep.registration)  (full form)
- AddWallet        → KycSuccess(KycStep.linkWallet)    (one-tap confirm)
- AlreadyRegistered → fall through to processStatus dispatch
- KycRequired      → KycUnsupportedStepFailure(null)   (edge case)

New `KycLinkWalletPage` + `KycLinkWalletCubit` render the streamlined
one-tap flow for the AddWallet branch: shows the existing account's
name plus the current wallet address (hexEip55), submits via
`RealUnitRegistrationService.registerWallet(userData)`, then triggers
`KycCubit.checkKyc()` which sees AlreadyRegistered and routes forward.

Drop the now-dead `markRegistrationSignProduced()` API and update its
two callers in `KycRegistrationPage` and `KycEmailPage` to just call
`checkKyc()`. Remove `isRegistered` from `RealUnitWalletStatusDto`
(the field is no longer parsed or read anywhere in lib/; update all
tests + fixtures to use the new `state` parameter).

ARB keys added (DE + EN, alphabetically sorted): kycLinkWalletTitle,
kycLinkWalletDescription, kycLinkWalletSubmit.
Update the stale `markRegistrationSignProduced()` example in
`docs/testing.md` so it instead seeds `RealUnitWalletService.getWalletStatus()`
with an `alreadyRegistered` fixture — mirroring the current routing path
after the cubit moved off the local sign-produced flag.

Add a failure-path test for `KycCubit._runCheckKyc` that mocks
`getWalletStatus()` to throw and asserts the outer `catch` surfaces a
`KycFailure` rather than wedging the state machine after the disclaimer
gate passes.

Eliminate the redundant `getWalletStatus()` round-trip in
`KycRegistrationPage` and `KycLinkWalletPage`/`KycLinkWalletCubit`. The
parent `KycCubit` now attaches the `RealUnitUserDataDto` to `KycSuccess`
and `KycPageManager` forwards it via constructor: the registration page
seeds its controllers synchronously in `initState` and the link-wallet
cubit starts directly in `Ready(userData)` with the `Initial`/`Loading`
states removed. Tests updated to drop their wallet-service mocks and
drive the pages via the constructor instead.

Replace the generic `KycUnsupportedStepFailure(null)` emission for the
`KycRequired` branch with a dedicated `KycRequiredFailure` state. Add a
matching `kycRequiredFailureMessage` i18n key (DE + EN, alphabetical) so
the user sees "complete your identity verification first" instead of
the "step (-) cannot be completed" fallback.
@TaprootFreak TaprootFreak marked this pull request as ready for review May 28, 2026 10:02
@TaprootFreak
Copy link
Copy Markdown
Contributor Author

Self-reviewed via independent subagent loop before flipping to ready:

  • Pass 1: state-driven KYC routing + KycLinkWalletPage reviewed. 4 SHOULD-findings: stale markRegistrationSignProduced() example in docs/testing.md, no failure-path test for getWalletStatus(), redundant getWalletStatus() round-trips between cubit and pages, KycRequired surfacing the generic "step (-)" error. 1 NIT (PR description inaccuracy, no code change).
  • Fix commit e8e2568 addressed all four:
    • Docs example updated.
    • kyc_cubit_test.dart gains a getWalletStatus() throws → KycFailure blocTest.
    • KycCubit is now the single caller of getWalletStatus(); KycSuccess carries realUnitUserData?; both KycRegistrationPage and KycLinkWalletPage accept it via constructor and dropped their own fetches. Eliminates the three-round-trip race window.
    • Dedicated KycRequiredFailure state + i18n key replaces the unhelpful KycUnsupportedStepFailure(null) for the KycRequired server state.
  • Pass 2VERDICT: PASS_CLEAN. flutter analyze clean, 48 tests in the touched files green, suite 2303/2304 (the one failure is a pre-existing home_golden_test.dart pixel diff, unrelated to this PR).

End state: bestehende Aktionäre überspringen das Form komplett (AlreadyRegistered), Multi-Wallet-User bekommen one-tap Add-Wallet (AddWallet), Erst-User sehen vorausgefülltes Form (NewRegistration), _registrationSignProduced Flag ist weg. Pair-PR API: DFXswiss/api#3782.

Base automatically changed from integration/realunit-registration to develop May 28, 2026 10:14
TaprootFreak added a commit that referenced this pull request May 28, 2026
## Summary

Integration branch bundling the RealUnit registration UX improvements so
they can land on `develop` as a single coordinated change. Individual
feature PRs target this branch as their base; once they have all merged
here, this PR is reviewed end-to-end and merged into `develop`.

## Member PRs

- **[#599](#599) —
feat(i18n): broaden purchase pre-flight message wording**
Renames the buy-screen banner from "Registrierung erforderlich" →
"Zusätzliche Angaben erforderlich" so KYC-completed users aren't told
they need to register again.
- **[#600](#600) —
feat(realunit): pre-fill registration form from wallet status**
Pre-populates the Aktionariat registration wizard from `GET
/v1/realunit/wallet/status`, replacing the empty-controllers experience
that today forces users to retype data the backend already holds. Pairs
with [DFXswiss/api#3782](DFXswiss/api#3782).

## Why bundle

Both PRs touch the same screen (RealUnit registration), share goldens,
and only deliver their full UX value together — the new wording
communicates the right expectation, and the pre-fill makes that
expectation accurate. Reviewing them as one end-to-end change at the
integration step prevents partial-merge regressions and keeps the
goldens in sync.

## Workflow

1. Open each member PR against `integration/realunit-registration`
2. Merge member PRs into this branch as they're approved
3. Review this PR end-to-end against `develop` (full diff = combined
member content)
4. Merge into `develop`

Empty seed commit will disappear once the member PRs merge in and the
branch diverges with real content.

---------

Co-authored-by: Blume1977 <jana.ruettimann@dfx.swiss>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…equired flows by wallet mode

Rename the wallet-status DTO and fold the standalone wallet service into
`RealUnitRegistrationService`:

- `RealUnitWalletStatusDto` -> `RealUnitRegistrationInfoDto` (wire shape
  unchanged), file renamed accordingly.
- `RealUnitWalletService.getWalletStatus()` -> merged into
  `RealUnitRegistrationService.getRegistrationInfo()` calling the new
  canonical `GET /v1/realunit/registration` path. The deprecated
  `/wallet/status` mirror exists API-side for back-compat but the app no
  longer references it.
- DI registration for the standalone wallet service removed; every consumer
  (`KycCubit`, `KycEmailVerificationCubit`, `SettingsUserDataCubit`, page
  factories, golden mocks) now injects `RealUnitRegistrationService`.

Wallet-mode signing-capability gate:

- New `KycSignatureUnsupportedFailure` terminal state, emitted by
  `KycCubit._runCheckKyc` when the API routes the user to
  `NewRegistration` / `AddWallet` (both require an EIP-712 signature) AND
  the current wallet is `WalletType.debug` (address+signature mode that
  cannot sign). `AlreadyRegistered` / `KycRequired` still pass through.
- New `KycSignatureUnsupportedPage` rendered by `KycPageManager`.
- `KycCubit` now takes `AppStore` (instead of `RealUnitWalletService`) so
  the cubit can inspect `wallet.walletType`. DI updated.
- i18n keys `kycSignatureUnsupportedTitle` /
  `kycSignatureUnsupportedDescription` added to both ARB files
  alphabetically; generated `lib/generated/i18n.dart`.

Docs:

- New `docs/wallet-modes.md` documents the three wallet modes, their
  signing capability, the gate location, and the pattern future
  sign-required features should follow.
- `CONTRIBUTING.md` "State Management" section links the new doc.
- `docs/testing.md` example updated to the new service / DTO names.

Tests:

- New tests in `kyc_cubit_test.dart` covering the gate for debug+
  `NewRegistration`, debug+`AddWallet`, and the pass-through for
  debug+`AlreadyRegistered`.
- New widget test `kyc_signature_unsupported_page_test.dart` and golden
  `kyc_signature_unsupported_golden_test.dart`.
- `getRegistrationInfo` HTTP-wiring tests moved into
  `real_unit_registration_service_test.dart` (Bearer JWT + path +
  ApiException), replacing the deleted standalone wallet-service test.
- Aggregate DTO test, email verification cubit/page test, settings
  user-data cubit/page test, golden mocks and golden tests all updated to
  the new types.
@TaprootFreak
Copy link
Copy Markdown
Contributor Author

Follow-up — two changes:

(1) Consume canonical API endpoint. DTO RealUnitWalletStatusDtoRealUnitRegistrationInfoDto. The standalone RealUnitWalletService was deleted and its single method folded into the existing RealUnitRegistrationService as getRegistrationInfo(), pointing at the new /v1/realunit/registration path (the paired API PR DFXswiss/api#3782 keeps /wallet/status as a deprecated: true mirror; the app no longer consumes the legacy path anywhere).

(2) Wallet-mode signature gate. Three wallet modes exist: software (auto-sign), bitbox (hardware-confirmation sign), debug (cannot sign — DebugWalletAccount.signMessage throws UnsupportedError). KycCubit._runCheckKyc now reads the active mode via the injected AppStore and, before dispatching to KycRegistrationPage / KycLinkWalletPage, surfaces a dedicated KycSignatureUnsupportedFailure state when the wallet cannot sign and the server state requires it (NewRegistration / AddWallet). AlreadyRegistered and KycRequired pass through normally for debug-mode users — those don't need signing.

The new KycSignatureUnsupportedPage shows the message "Dieses Feature ist nur mit BitBox oder Software-Wallet verfügbar und kann im Debug-Modus (Adresse + Signatur) nicht getestet werden." (DE) / English counterpart.

Docs: new reference docs/wallet-modes.md linked from CONTRIBUTING describes the three modes, their signing capability, and the gate pattern for future sign-required features.

  • Subagent review: VERDICT: PASS_CLEAN after one NIT (stale doc-comment path) fixed.
  • flutter analyze clean, 2210/2210 non-golden tests + 99/99 golden (the one home-page pixel-diff is pre-existing and unrelated).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant