Skip to content

fix(kyc): filter country pickers by allow-flag, harden load states#519

Merged
TaprootFreak merged 2 commits into
developfrom
fix/country-field-kyc-consistency
May 23, 2026
Merged

fix(kyc): filter country pickers by allow-flag, harden load states#519
TaprootFreak merged 2 commits into
developfrom
fix/country-field-kyc-consistency

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

The shared CountryField widget (used by 4 KYC/onboarding screens — nationality, registration personal step, registration address step, settings address) had three correctness gaps, all fixed here:

  • No allow-flag filtering. Every picker showed the full unfiltered country list, so a user could pick a country the backend rejects. The residence/address pickers now filter by kycAllowed. The nationality pickers intentionally do not filter — see below.
  • Loading/error collapsed the field. ConnectionState.waiting rendered SizedBox.shrink() and hasError rendered a bare Text with the raw exception. With no FormField in the tree, Form.validate() returned true with no selection and the submit crashed on country!. Both states now render a present, invalid FormField; the error state shows a localized message and a retry.
  • Silent Switzerland default. _preloadCountry auto-selected the first country, which made the validator dead code and silently registered users as Swiss nationals. Removed — selection is now explicit and validated.

Also: the non-localized 'Set Nationality failed: …' SnackBar is now localized.

Flag semantics (resolved against the backend)

A deep audit of DFXswiss/api resolved which flag actually gates which field — the DTO flag names are misleading:

  • Address/residence country → kycAllowed (DTO alias of the backend's dfxEnable). A non-allowed address country is rejected with HTTP 400 on submit, so client-side filtering is correct and useful here.
  • Nationality → not filtered. No CountryDto field correctly represents the nationality gate: nationalityAllowed maps to nationalityStepEnable, which the backend reads nowhere; the real gate is nationalityEnable, which is not exposed. And a disallowed nationality is routed to manual review, not rejected — nationality is a fact, so the picker must offer every country. See CountryDto: nationalityAllowed maps to an unused column; no DTO field exposes the real nationality gate DFXswiss/api#3755.
  • locationAllowed was the original (wrong) choice for residence — it maps to ipEnable and gates the request IP's country, not a user-entered field.

Out of scope

The KYC flow can collect nationality twice (standalone nationalityData step + the registration step). The root cause is on the backend — tracked in DFXswiss/api#3754. Not touched here.

Test plan

  • flutter analyze — clean
  • flutter test — all tests pass; test/widgets/form/country_field_test.dart covers residence filtering by kycAllowed, the nationality picker showing all countries, loading/error invalidity, retry recovery, and no auto-select
  • Manual: residence picker in KYC onboarding shows only kycAllowed countries, nationality picker shows all, both block submit until a country is picked, and recover from a failed country load via retry

@TaprootFreak

Copy link
Copy Markdown
Contributor Author

Review-Notiz (Draft-Stand)

Verdict: Logisch sauber, gute Test-Coverage, ready-for-review sobald rebased.

Was ich gegen merge-base(develop, head)..head (echter Intent) gesehen habe

  • DfxCountryDto trägt bereits nationalityAllowed / locationAllowed (Backend-seitig vorhanden), Country.toCountry() reicht sie jetzt durch (lib/packages/service/dfx/models/country/country.dart:13-19).
  • CountryFieldPurpose.allows() mit Switch ist die einzige Mapping-Stelle und expliziter Single-Source-of-Truth — gut (lib/widgets/form/country_field.dart:11-21).
  • Alle 4 Callsites werden konsistent angepasst (Nationality-Page, Registration-Personal-Step, Registration-Address-Step, Settings-Edit-Address-Page). Es gibt im Code keinen weiteren CountryField-Caller (grep verifiziert). Bank-Account-Add hat kein Country-Feld, also kein blinder Fleck.
  • _StatusField mit immer-invalid FormField<Country>(validator: (_) => '') schließt die "Form.validate() == true ohne Selection"-Lücke. Sehr nett: dasselbe Pattern hält im Retry-Loop (waiting → error → waiting).
  • Stiller "Schweiz auto-select" weg — _preloadCountry ist tot, validator ist jetzt scharf. Compliance-relevant, korrekt.
  • Test-Coverage (test/widgets/form/country_field_test.dart, 227 Zeilen) deckt: purpose-Filter (nat vs res), no-auto-select, no-preselection-onChanged, surrounding-Form-invalid-while-loading, error-state + retry-recovery + invalid-during-error. Vollständig gegen die Schwerpunkte.
  • SnackBar-String 'Set Nationality failed:' wird zu lokalisiertem setNationalityFailed(message)feedback_no_fallbacks sauber: keine ?? [] / silent defaults, snapshot.data!.where(...) ist nach error-branch sicher.

Architektur-Konsistenz

  • Country-Service-Caching ist "cache forever" (cachedCountries: List<Country>?) — identisch zu dfx_legal_document_service und dfx_company_info_service. Kein TTL/invalidate wie es der Review-Brief vermutet hat, aber konsistent mit dem Sibling-Pattern, also kein Issue.
  • Threat-Model: Filter ist UI-Defense-in-Depth, Backend bleibt Authority — wenn Backend nationalityAllowed:true für ein Sanctioned Country liefert, zeigt die App es. Das ist der gewollte Modus aus docs(architecture): codify 'API as Decision Authority' rule + audit + plan #491 ("API as Decision Authority"), nicht ein Bug.

Was DRAFT noch blockiert

  1. Stale base. git diff origin/develop..pr-519 enthält Phantom-Reverts (handbook-Maestro-Yamls, home_page.dart highlightedSemanticsId, text_substring_highlighting.dart, tier3-handbook.yaml, docs/screens.md) weil 61de19b (Handbook 7 new flows) nach dem Branch-Off auf develop landete. Vor "Ready" bitte git rebase origin/develop — sonst geht ein staler Branch mit großem Phantom-Diff als Regression durch (genau das Risiko aus feedback_pr_two_dot_diff_check).
  2. Manueller Test-Checkbox im PR-Body ist noch leer — sobald rebased + manueller Smoke OK, ready-for-review.

Kein Code-Fix nötig.

The shared CountryField (used by 4 KYC/onboarding screens) showed the
same unfiltered country list for both nationality and residence
pickers, auto-selected Switzerland, and collapsed to an empty or
non-FormField widget while loading or on error — letting
Form.validate() pass with no selection and crashing the KYC submit on
a null country.

- Country now carries nationalityAllowed/locationAllowed; CountryField
  takes a required `purpose` and filters the list accordingly
- loading and error states render a present, invalid FormField so the
  surrounding form can never validate without a selection; the error
  state shows a localized message and a retry instead of the raw
  exception
- drop the silent Switzerland auto-select; country selection is now
  explicit and validated
- localize the nationality-failure SnackBar
A backend audit of DFXswiss/api showed the allow-flags this branch
originally filtered by are wrong: the CountryDto `locationAllowed`
field is `ipEnable` (it gates the request IP's country, not a
user-entered country), and `nationalityAllowed` maps to the entity
column `nationalityStepEnable`, which the backend reads nowhere.

- the residence/address picker now filters by `kycAllowed`
  (= `dfxEnable`), the flag the backend actually enforces on the
  address country (HTTP 400 on submit otherwise)
- the nationality picker no longer filters: nationality is a fact,
  and the backend routes a disallowed nationality to manual review
  rather than rejecting it, so hiding countries would be wrong
- Country drops nationalityAllowed/locationAllowed, carries kycAllowed
@TaprootFreak TaprootFreak force-pushed the fix/country-field-kyc-consistency branch from 0e208a4 to 82154ae Compare May 23, 2026 09:33
@TaprootFreak TaprootFreak marked this pull request as ready for review May 23, 2026 10:04
@TaprootFreak TaprootFreak merged commit 214f5b2 into develop May 23, 2026
9 checks passed
@TaprootFreak TaprootFreak deleted the fix/country-field-kyc-consistency branch May 23, 2026 10:17
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