refactor(app): consume UserCapabilities + ALREADY_REGISTERED registration status#497
Conversation
…g queue Companion to the seven PRs that landed today (RealUnitCH#491, RealUnitCH#492, RealUnitCH#493, RealUnitCH#494, RealUnitCH#495, RealUnitCH#496, RealUnitCH#497 + DFXswiss/api#3732, #3733). Adds: - A 'Shipped (2026-05-21)' table mapping each pair-PR to the V-IDs it closes, with the W2 pair flagged as the closure of the 2026-05-21 ident-misroute incident. - An 'Outstanding — next phase' section grouping the remainders: P0 follow-ups (V6c/V6d/V16/V20), Wave 4 (V17/V18/V14/V19 — new backend modules), and the P1/P2 long tail (V21–V27, V29–V33). Each item carries the same V-ID anchor used elsewhere so PR reviewers can cross-reference cleanly. Does not touch V<N> classifications or counts — those stay as the reviewer set them. Pure append at the end of the file.
995e17d to
7edd48f
Compare
Review-Findings (Wave 3 — UserCapabilities + ALREADY_REGISTERED)Blocker1. Listener im Reproduktion: Wallet schon registriert → BitBox-Sign abschliessen → API liefert Vorschlag: Den Branch verallgemeinern. Jeder 2. Vorschlag: Empfehlungen3. Vorschlag: Im 4. Was solide ist
Geprüft und nicht beanstandet: Magic-String-Mapping ( |
|
Habe die zwei Blocker direkt im Branch gefixt, weil @Blume1977 gerade nicht aktiv ist. B1 — Listener (
|
…tion status Wave 3.2 of the API-as-Decision-Authority audit (`docs/api-authority-plan.md`), companion to API PR DFXswiss/api#3733. The app's settings + registration cubits used to derive UI gating from KYC step status and silently swallow 'already registered' 400s as success. Both signals are now first-class fields on the API response. DTO mirrors ----------- - `UserDto` (`/v2/user`) gains `capabilities: UserCapabilitiesDto` with `canEditName / canEditMail / canEditPhone / canEditAddress / supportAvailable`. All flags default to `false` so a pre-#3733 backend degrades to 'no edit affordances' rather than offering an action the backend would reject. - `RegistrationStatus.alreadyRegistered` mirrors the new `RealUnitRegistrationStatus.ALREADY_REGISTERED` enum value. Consumers --------- - `settings_contact_cubit` reads `userDto.capabilities.supportAvailable` instead of `mail != null`. State field renamed `emailSet` → `supportAvailable` for consistency. - `settings_user_data_cubit` now fetches `/v2/user` alongside `/v2/kyc` and exposes `capabilities` on its Success state. The `SettingsUserDataPage` wires each row's `onEdit` to `canEditName / canEditPhone / canEditAddress`; rows without capability omit the Edit button. `_UserDataRow` drops its `statusLabel == null` co-gating — the badge is informational, the capability is the authority. - `settings_edit_name_cubit` drops the `currentStep?.status == inReview` interpretation. The upstream capability gate prevents the cubit from being instantiated when editing is forbidden, and the pending branch now only fires defensively when the session lacks a URL. - `kyc_registration_submit_cubit` no longer treats *any* `ApiException` after a successful sign as `Success(completed)`. Backend rejection of an already-registered wallet is now a structured `Success(alreadyRegistered)` from the API, and `KycCubit.checkKyc` resolves the next step from there. All other ApiExceptions surface as failures as they always should. Tests ----- - `settings_contact_cubit_test`: assertions renamed and now exercise the API `supportAvailable` capability fixture rather than mail presence as a proxy. - `settings_user_data_cubit_test`: every fixture now mocks `kycService.getUser()` (the new third call site) and seeds `capabilities` where relevant. - `settings_edit_name_cubit_test`: "inReview → Pending" case rephrased to "no URL → Pending". The "no URL → Failure" case is inverted to Pending (matches the cubit's new defensive branch). - `kyc_registration_submit_cubit_test`: the "swallow-ApiException-as-success" case is replaced by an explicit "backend returns alreadyRegistered status → Success(alreadyRegistered)" and a complementary "ApiException → Failure" case so the silent-mask pattern doesn't regress. Verification ------------ - `flutter analyze` — clean - `flutter test` — **1417 / 1417 passing**
…w/forwardingFailed weiterfuehren Der RegistrationPage-Listener hat bisher nur fuer RegistrationStatus.completed markRegistrationSignProduced() + checkKyc() gerufen. Mit Wave 3.2 emittiert der Cubit aber ueber den Bind an _signEip712 fuer jeden Backend-Status (completed, pendingReview, forwardingFailed, alreadyRegistered) einen Success — der Sign hat in jedem Fall stattgefunden. Vorher fiel der alreadyRegistered-Path durch und der Flow hing nach BitBox-Sign auf der Registration-Seite. Die Branch-Bedingung entfaellt jetzt: jeder Success lift das Sign-Gate und triggert checkKyc(). Fuer forwardingFailed wird zusaetzlich ein informativer SnackBar gezeigt, damit der User sieht, dass die API-Weiterleitung verzoegert ist (Retry uebernimmt das Backend). Tests: drei neue testWidgets fuer alreadyRegistered, pendingReview und forwardingFailed. Der bestehende completed-Test verifiziert jetzt zusaetzlich markRegistrationSignProduced().
…tAddress-Gating umstellen Wave 3.2 hat den inReview-Branch und das url==null-Throw aus settings_edit_name_cubit entfernt — der gleiche Code stand aber noch im settings_edit_address_cubit. Die SettingsUserDataPage gateet den Address-Edit-Button bereits ueber capabilities.canEditAddress, also war V15 nur halb geschlossen: lokale Capability-Logik stand neben der API-Authority. Cubit jetzt deckungsgleich mit dem Name-Cubit: - kein KycStepStatus.inReview-Branch mehr (die parent page invokes diesen Cubit nur wenn canEditAddress true ist). - url==null fuehrt zu SettingsEditAddressPending (defensive Branch fuer die Race zwischen Capability-Check und startStep) statt zu einer Exception. Tests entsprechend angepasst — "inReview --> Pending" durch "no URL --> Pending" ersetzt und der "no URL --> Failure"-Case zu Pending gewandelt.
…lossen Die V46-Markierung "Closed by W3.1 / W3.2 (capabilities)" passte nicht zum PR-Diff: kyc_registration_personal_step.dart wird in diesem Wave nicht angefasst, und die zugrundeliegende Quelle des Dropdowns ist RegistrationUserType.values (lokales Enum) — nicht eine Capability-Liste aus UserCapabilitiesDto. V46 deckungsgleich mit V12/V13: braucht eine eigene API-Aenderung (Capability-Feld availableUserTypes oder eigener Endpoint) plus UI-Migration. Bis dahin als deferred fuer den naechsten Wave gefuehrt, damit der Audit-Status mit dem Code-Stand uebereinstimmt.
bdfecc7 to
577abac
Compare
|
Rebased + Polish-Pass eingespielt. Rebase auf
Neue SHAs:
Polish-Commit:
PR-Body aktualisiert: neuer Abschnitt "Post-Review Fixes" listet jetzt die Listener-Generalisierung in Lokal verifiziert:
Force-Push erfolgte mit |
…ordering in-app (#499) (#530) ## Summary Reverts #499 ("refactor(reference-data): consume /v1/legal-document, /v1/company-info, country.displayOrder"). The companion API-side PR (DFXswiss/api#3734) has been **closed unmerged** — the decision is to keep legal-document URLs, company contact info and country ordering **client-side in the app** rather than serving them from the API. `/v1/legal-document` and `/v1/company-info` do not exist in the API, so the app owns this data. ## What this restores - `legal_documents_config.dart` — the hardcoded registration-agreement PDF URLs (`_registrationAgreementPdfUrls`, de/en) and the `pdfUrls`-based `LegalDocumentConfig`. - `settings_contact_page.dart` — the hardcoded RealUnit company contact tiles (phone, email, website, imprint). - `country_field.dart` — the hardcoded `['CH','DE','IT','FR']` priority ordering. ## What this removes - `DfxLegalDocumentService`, `DfxCompanyInfoService` and their DTOs (+ tests) — the API-consumption layer added by #499. - The `Country.displayOrder` field and the `/v1/legal-document` + `/v1/company-info` wiring. ## Not affected DFXswiss/api#3732 and #3733 stay merged in the API; their app companions (#494 KYC routing, #497 capabilities) are untouched. Only the reference-data refactor is reverted. ## Verification - `flutter analyze` — No issues found. - `flutter test` — all 1504 tests pass.
…lessons (#593) Companion to [DFXswiss/api#3773](DFXswiss/api#3773). The David-review back-and-forth on the Wave-3 reset produced a concrete set of rules that should bind every future capability consumption in this repo — this PR writes them down so the next contributor doesn't have to rediscover them. ## Three docs updated ### 1. \`CONTRIBUTING.md\` — new sub-section "Consuming API capabilities — eight rules" Lives inside the existing "API as Decision Authority — CRITICAL" section. Covers: 1. Read the capability shape, don't reconstruct it 2. Tile/button visibility for discoverable actions is unconditional 3. Map prerequisite types to UI components, not to business rules 4. Legacy backend tolerance — capability optional, sane fallback 5. No reactive 400-handling for what a capability could pre-tell 6. Pair-PR discipline 7. Tests pin the contract, not the implementation 8. Push back on capability shape that's over-engineered Cross-references the API-side mirror in [\`DFXswiss/api:CONTRIBUTING.md\`](https://github.com/DFXswiss/api/blob/develop/CONTRIBUTING.md). ### 2. \`docs/api-authority-plan.md\` — Wave-3 lessons-learned block Documents the **six-PR** history of V9 (a single capability flag): | PR | Direction | Outcome | |---|---|---| | [api#3733](DFXswiss/api#3733) | API: \`+supportAvailable: bool\` | merged | | [app#497](#497) | App: consume \`supportAvailable\` bool | merged | | [app#588](#588) | App: unconditional Support tile | merged | | [api#3761](DFXswiss/api#3761) | API: \`-supportAvailable: bool\` | merged | | [api#3767](DFXswiss/api#3767) | API: ActionCapability tree (4 DTOs, 170 LOC) | **closed without merge** | | [api#3772](DFXswiss/api#3772) | API: \`createSupportTicket: { available, missingPrerequisite? }\` (91 LOC) | merged | Plus three "what we'd do differently" points and forward guidance for Waves 4 and 5. ### 3. \`docs/api-authority-audit.md\` — V9 closed Marked as closed by api#3772 with a link back to the lessons-learned section (because the linear "closed by W3" mapping in the audit table doesn't capture the actual non-linear history). ## Why now Cyrill asked for these rules to be prominently documented in both repos and in cross-repo working notes so the pattern doesn't drift on the next capability. The API side gets the mirror PR ([DFXswiss/api#3773](DFXswiss/api#3773)). ## Verification - Pure documentation — no code changes - Markdown renders cleanly (verified) - Cross-references resolve ## Companion PRs - API rules: [DFXswiss/api#3773](DFXswiss/api#3773) - App pair-PR consuming the first capability following this pattern (\`createSupportTicket\`): opening shortly on \`refactor/consume-support-capability\`.
Summary
Wave 3.2 of the API-as-Decision-Authority audit (
docs/api-authority-plan.md), companion to API PR DFXswiss/api#3733. The app's settings + registration cubits used to derive UI gating from KYC step status and silently swallow "already registered" 400s as success. Both signals are now first-class fields on the API response.Changes
DTO mirrors
UserDto(/v2/user) gainscapabilities: UserCapabilitiesDtowithcanEditName / canEditMail / canEditPhone / canEditAddress / supportAvailable. All flags default tofalseso a pre-#3733 backend degrades to "no edit affordances" rather than offering an action the backend would reject.RegistrationStatus.alreadyRegisteredmirrors the newRealUnitRegistrationStatus.ALREADY_REGISTEREDenum value.Consumers
settings_contact_cubitreadsuserDto.capabilities.supportAvailableinstead ofmail != null. State field renamedemailSet→supportAvailablefor consistency with the API.settings_user_data_cubitnow fetches/v2/useralongside/v2/kycand exposescapabilitieson its Success state. TheSettingsUserDataPagewires each row'sonEdittocanEditName / canEditPhone / canEditAddress; rows without capability omit the Edit button._UserDataRowdrops itsstatusLabel == nullco-gating — the "Change in review" badge stays as an informational label, the capability is the authority.settings_edit_name_cubitdrops thecurrentStep?.status == inReviewinterpretation. The upstream capability gate prevents the cubit from being instantiated when editing is forbidden, and the pending branch now fires only defensively when the session lacks a URL.kyc_registration_submit_cubitno longer treats anyApiExceptionafter a successful sign asSuccess(completed). Backend rejection of an already-registered wallet is now a structuredSuccess(alreadyRegistered)from the API, andKycCubit.checkKycresolves the next step from there. All other ApiExceptions surface as failures as they always should.Post-Review Fixes
Two follow-up commits address gaps surfaced during review of the original Wave 3.2 commit:
kyc_registration_page.dartlistener generalisation (fix(kyc): KYC-Flow nach Sign auch fuer alreadyRegistered/pendingReview/forwardingFailed weiterfuehren) — the original listener only invokedmarkRegistrationSignProduced()+checkKyc()forRegistrationStatus.completed. With Wave 3.2's cubit emittingSuccessfor every backend status the sign produces (completed / pendingReview / forwardingFailed / alreadyRegistered), branching by status caused thealreadyRegisteredBitBox-sign path to hang on the registration page. The listener now lifts the sign gate and triggerscheckKyc()on everySuccess;forwardingFailedadditionally surfaces an informativeregistrationForwardingFailedSnackBar (new i18n key in DE + EN). Three newtestWidgetscover the previously-uncovered statuses.settings_edit_address_cubitmigration (refactor(settings): edit-address-cubit analog zu edit-name auf canEditAddress-Gating umstellen) — V15 was only half-closed:SettingsUserDataPagealready gates the Address-Edit button viacapabilities.canEditAddress, but the address cubit still carried the oldcurrentStep?.status == inReviewbranch +url == nullthrow. Cubit now mirrorssettings_edit_name_cubit: noinReviewinterpretation,url == nullresolves toSettingsEditAddressPending(defensive race branch) instead of throwing. Tests rephrased analogous to the name-cubit pass.Audit alignment
docs/api-authority-audit.mdV46 (registration-personal-step user-type dropdown) was previously marked "Closed by W3.1 / W3.2 (capabilities)" but is not in this PR's diff. The dropdown reads from a localRegistrationUserType.valuesenum, not fromUserCapabilitiesDto, so closing V46 requires a separate API change (availableUserTypescapability or registration-endpoint extension). Re-marked as Deferred to a follow-up wave for accuracy.Backwards compatibility
capabilitiesfield defaults tofalseeverywhere — old backends silently disable Edit and Support affordances rather than offering unbacked actions.RegistrationStatus.alreadyRegisteredis a new enum value the old backend never sends — old client code paths that fall throughswitchwould have hit the unhappy path the same way.Tests
settings_contact_cubit_test: assertions renamed and now exercise the APIsupportAvailablecapability fixture rather than mail presence as a proxy.settings_user_data_cubit_test: every fixture now mockskycService.getUser()(the new third call site) and seedscapabilitieswhere relevant.settings_edit_name_cubit_test/settings_edit_address_cubit_test: "inReview → Pending" case rephrased to "no URL → Pending". The "no URL → Failure" case is inverted to Pending (matches the cubits' new defensive branch).kyc_registration_submit_cubit_test: the "swallow-ApiException-as-success" case is replaced by an explicit "backend returns alreadyRegistered status → Success(alreadyRegistered)" and a complementary "ApiException → Failure" case so the silent-mask pattern doesn't regress.kyc_registration_page_test: three newtestWidgetscoveralreadyRegistered / pendingReview / forwardingFailed; the existingcompletedcase additionally verifiesmarkRegistrationSignProduced().Verification
develop(wasrebaseable: false; rebase landed cleanly — no conflicts in buy/sell cubits, workflows, or anywhere else).flutter analyze— cleanflutter test— 1438 / 1438 passingCloses (audit, P0/P1)
Tracked in
docs/api-authority-audit.md.