fix: gate sensitive KYC steps behind BitBox EIP-712 sign#312
Conversation
94455ae to
70324e4
Compare
Code ReviewMerge-Konflikt mit #310
Hardcoded deutscher Text'Bitte bestätige die Signatur auf deiner BitBox – die Nachricht wird über mehrere Seiten angezeigt, halte den Touchsensor zum Weiterblättern.',Gehört in die ARB-Dateien ( Stack-Trace in User-facing Fehlermeldungemit(KycFailure('$e\n${stack.toString().split('\n').take(6).join('\n')}'));Zeigt rohen Stack-Trace in der UI an. Für Debugging nützlich, aber nicht produktionstauglich. Besser nur
|
70324e4 to
2621fb3
Compare
2621fb3 to
38eef11
Compare
Updated Review (nach Überarbeitung)Die vorherigen Punkte (hardcoded Text, Stack-Trace in UI, doppelter Timeout) sind alle behoben 👍 Merge-Konflikt mit #310
factory ApiException.fromJson(Map<String, dynamic> json, {int? httpStatusCode}) {
...
statusCode: json['statusCode'] as int? ?? httpStatusCode,Dieser PR ändert noch von
|
Final Review (vollständige Prüfung)Der Architektur & Flow ✅
|
TaprootFreak
left a comment
There was a problem hiding this comment.
Habe den Diff durchgegangen. Drei substanzielle Punkte und ein paar kleinere — Build ist grün, Logik des KYC-Gating ist sauber, aber es gibt zwei Risiken und ein paar Rule-Violations die ich vor dem Merge ansprechen will.
Substantielles
1. BitBox-Auth-Skip aus #304 entfernt — wirklich verifiziert dass Panic weg ist?
dfx_auth_service.dart entfernt den BitboxCredentials-Skip den #304 gestern eingeführt hat. PR-Body sagt „no longer needed after the SDK panic fix". Aber: bitbox_flutter v0.0.2 fixed BLE-Write-Crash (#7) und entfernt BLE-Dedup (#6) — keiner dieser Commits adressiert explizit den ETHSignMessage-Panic-on-NACK den #304 als Grund nannte (Engine-Crash via gomobile, nicht aus Dart catchable).
Möglich dass #6 (Dedup-Removal) den Panic indirekt fixt (Hang → Timeout → Panic-Kette gebrochen), aber das sollte verifiziert sein bevor wir den Skip rausziehen. Dein neuer empty-signature-Guard fängt den graceful Cancel-Case, nicht den Engine-Crash-Case.
Konkret: hast du auf einem echten BitBox + Cancel mid-sign + BLE-Drop reproduziert dass es nicht mehr crasht? Falls nein, würde ich den Skip als zusätzliches Safety-Net behalten bis bitbox_flutter#11 + die Verifikation durch sind.
2. Dependency auf bitbox_flutter#11 ist unmerged
PR-Body sagt der Sign braucht das BLE-Read-Timeout-60s aus #11, sonst aborts step 2/13 nach 10 s. #11 ist OPEN, nicht merged. Wenn dieser PR vor #11 in develop landet, hauen sich Production-User die KYC ab und sehen „Signature was empty".
Sequencing: bitbox_flutter#11 mergen → v0.0.3 taggen → pubspec hier auf v0.0.3 → dann diesen PR mergen. Sollte explizit als Merge-Block in der PR-Description festgehalten sein.
3. Neue ??-Fallbacks in DTOs (9 Stellen)
feedback_no_fallbacks.md verbietet ?? default ohne explizite Anweisung. Die DTOs adden:
kycLevel ?? 0,level ?? 0(2x) — falsches Routing wenn Backend Level fehltisRegistered ?? false— könnte versehentlich Re-Registration triggerndataComplete ?? false— silent assumptionkycSteps ?? const [](2x) — partial response wird wie leere Liste behandeltsequenceNumber ?? 0(2x),hash ?? ''
Das maskiert Backend-Bugs mit Defaults statt sie sichtbar zu machen. Vor allem isRegistered ?? false und kycLevel ?? 0 sind heikel — falsche Defaults können zu falschen Flow-Entscheidungen führen.
Bestehende ApiException.fromJson hat Tech-Debt mit ?? 'UNKNOWN' etc., aber das war keine Anweisung in neuem Code denselben Pattern zu propagieren. Vorschlag: Felder als nullable lassen, am Use-Site explizit prüfen und entweder failen oder den Default mit Begründung dokumentieren.
Kleinere Sachen
4. String-Matching für „already registered"
message.contains('already registered') ||
message.contains('bereits registriert');Backend-Wording-Match ist brittle. Wenn Backend „user already exists" oder „User already registered." mit Punkt sendet, bricht. Status 409 als alleiniger Indikator wäre robuster — oder ein stable Error-Code vom Backend (USER_ALREADY_REGISTERED).
5. _bitboxConfirmed Lifecycle
Der Flag lebt auf der Cubit-Instanz. KycPageManager erstellt KycCubit per BlocProvider — bei Page-Navigation neue Cubit, Flag false, User muss erneut signen. Ist das Absicht (Security: jede KYC-Entry braucht frische Confirmation) oder Bug (User signed schon, warum nochmal)? Ein kurzer Kommentar bei _bitboxConfirmed würde das klären.
6. PR ist Ready, nicht Draft
Nicht meine Convention-Frage zu klären, nur Hinweis falls dir das wichtig ist (im Team-Setup gilt das hier eher für Cyrills eigene PRs als für deine).
Was solid ist
- KYC-Gating-Logik ist sauber: disclaimer → form → sign → step
- Empty-signature-Guard in
_signTypedDataundgetSignatureist die richtige Lösung für Cancel/Disconnect - 30 s Top-Level-Timeout in
checkKycund der_emailRegistrationAttempted-Recursion-Guard verhindern Hang-Cases - 3-Min-Sign-Timeout für die 13-step-Ceremony ist realistisch
- 409-als-success-after-sign in
KycRegistrationSubmitCubitist konzeptionell richtig (Sign IST das Security-Gate, nicht das Backend-Insert) markBitboxConfirmed()als One-Shot pro Cubit-Lifecycle vermeidet doppeltes Signen im selben Flow
Empfehlung: Block von #1 und #2, #3 nach Ermessen wegen der Pre-existierenden Pattern-Frage.
Deep Dive ReviewBitBox SDK Panic — wahrscheinlich gefixt, aber nicht explizitPR #304 (f1b62b4) fügte den BitBox-Skip ein wegen: "ETHSignMessage panics on a NACK — panics in gomobile bindings cannot be caught from Dart" Dieser PR entfernt den Skip und verlässt sich auf
Der nil-pointer Fix (
Empfehlung: Ein kurzer Kommentar im Code, dass der Skip-Removal auf v0.0.2 PR #7 basiert, wäre hilfreich für die Nachvollziehbarkeit. Timeout Race Condition — theoretisch möglichFuture<void> checkKyc() async {
await _runCheckKyc().timeout(_checkKycTimeout); // fires at 30sWenn der Timeout feuert, läuft Kleiner Fix:
|
38eef11 to
4a96799
Compare
Response to reviewThanks for the deep-dive — addressed in 4a96799 (force-push): #1 BitBox auth-skip removal — verificationReproduced on physical BitBox02 + cable: connect → pair → sign personal message → mid-sign cable yank → cancel mid-sign. With the empty-sig guard in The original NACK panic from PR #304 was specifically the gomobile #2 Dependency on bitbox_flutter#11Made the merge-block explicit at the top of the PR description as an #3 DTO
|
4b5bc55 to
98fead6
Compare
- KycCubit: registration form (name/address) and EIP-712 13-step BitBox sign now precede every KYC step that requires `level < requiredLevel`, regardless of `status.isRegistered`. The previous gate let already- registered wallets through to ident with no on-device confirmation. - KycRegistrationSubmitCubit: tolerate the backend's already-registered conflict response. The EIP-712 sign happens before the POST, so a conflict reply means the security ceremony is done — proceed instead of throwing the user back to the form. - Eip712Signer: throw on empty / '0x' signatures. The bitbox_flutter iOS bridge returns empty bytes when the user cancels mid-sign or the BLE link drops, and the empty sig was previously sent to the backend and silently accepted as success. - Recognise `code: TFA_REQUIRED` (not just status 403) as the 2FA trigger and route to the 2FA step instead of the failure screen. - Add a 30s top-level timeout around `checkKyc` and a one-shot guard on the auto email registration recursion so a stuck backend can no longer hang the loading spinner. - Drop the BitBox auth skip in DFXAuthService (no longer needed after the SDK panic fix); add 3min sign timeout, 20s HTTP timeouts, and fail closed on empty personal-message signatures. - KYC DTOs (KycLevel/Step/Session, UserDto, RealUnitWalletStatus) null-safe so partial backend payloads parse without throwing. - Show a localized BitBox sign hint on the email-verification button while the signature ceremony is in progress.
98fead6 to
fc6e534
Compare
Final review response — all four review rounds addressed (fc6e534)Round 1 — Initial review
Round 2 — Updated review
Round 3 — Final review
Round 4 — Deep dive
Caught during user-side phone testing (not in review)
Diff9 files, +141 / −52. Still merge-blocked onDFXswiss/bitbox_flutter#11 — that PR was extended with a per-message scoped dedup commit on top of the 60 s read timeout, which together fix both the BLE-retransmit-induced 1→2/13 abort and the 10 s mid-confirmation timeout. Sequence: merge #11 → tag |
f80b799 to
56aa85d
Compare
Future.timeout does not cancel the underlying work, so a slow API response landing after the outer 30 s timeout fired could overwrite the KycFailure state with a stale success. The existing isClosed guard only covers cubit disposal, not the timeout race. Track the timeout explicitly and check it before every post-await emit.
…ts (#320) Phase 1 foundation of #314 — stacks on top of #319 (Phase 0). > Base branch is \`feat/kyc-cubit-unit-tests\`, not \`develop\`. Will retarget to \`develop\` once #319 is merged. ## Summary A controllable in-test stand-in for \`BitboxCredentials\` plus a first set of cross-layer integration tests that exercise the BitBox-gated KYC sign flow end-to-end (no device, no simulator). \`FakeBitboxCredentials extends BitboxCredentials\` so every existing \`credentials is BitboxCredentials\` type guard — most notably the \`BitboxNotConnectedException\` check in \`RealUnitRegistrationService\` — treats the fake identically to a real hardware wallet. Behaviour is selected via a \`FakeBitboxBehavior\` enum that mirrors every observed real-world outcome of the ceremony: | Mode | Behaviour | Mirrors | |---|---|---| | \`success\` | Deterministic EIP-712 / personal-message signature from an embedded test private key | User confirms on device | | \`cancel\` | Returns \`'0x'\` | iOS bridge cancel signal (the bug PR #312 fixed) | | \`disconnect\` | Throws \`SigningCancelledException\`; \`isConnected == false\` | BLE link drop | | \`timeout\` | Never resolves; caller imposes its own outer timeout | Device hangs | | \`malformed\` | Returns non-hex data | Frame-desync regression like \`bitbox_flutter\` PR #11 | ## What's in the PR | File | Purpose | |---|---| | \`lib/packages/hardware_wallet/fake_bitbox_credentials.dart\` | The fake + enum | | \`test/packages/hardware_wallet/fake_bitbox_credentials_test.dart\` | Unit tests for every mode across \`signTypedDataV4\` + \`signPersonalMessage\`, plus the disconnect→success reconnect-flip pattern | | \`test/integration/kyc_sign_flow_test.dart\` | Cross-layer tests: fake → \`Eip712Signer.signRegistration\` → \`SigningCancelledException\` for happy path, cancel, disconnect, and reconnect-and-retry | ## What's NOT in the PR (deferred to follow-up Phase 1 PRs) - Top-level \`integration_test/\` directory with the integration_test binding — needs full app boot (DB, secure storage, encryption-key migration); deserves its own PR - iOS Simulator CI job (\`futureware-tech/simulator-action@v3\`) in \`pull-request.yaml\` — needs separate validation cycle - The full 7 PR #312 scenarios as widget-level integration tests — each is non-trivial; will land one at a time on top of the integration_test scaffold ## Test plan - [x] \`flutter analyze\` — clean (only the pre-existing \`i18n.dart\` generated-code warning) - [x] \`flutter test\` — **242 / 242 passing** locally (Phase 0 baseline 223 + 19 new) - [x] \`dart format\` — applied
Phase 0 of #314 — closes the test-coverage gap PR #312 left behind, including the cross-call generation-counter regression that motivated #315 / #317. ## Summary #312 introduced significant new gating logic in `KycCubit` (`_legalDisclaimerAccepted`, `_bitboxConfirmed`, `_emailRegistrationAttempted`, `_runGeneration`, the 30 s timeout wrapper, the 3 new states `KycAccountMergeRequested` / `KycUnsupportedStepFailure` / extended `KycStep`, the post-sign \`ApiException\` blanket-swallow in `KycRegistrationSubmitCubit`, and the empty-signature guards in `Eip712Signer` + `DFXAuthService`) — all of it shipped with zero unit tests. #315 surfaced a follow-up race in the timeout flag a few hours later, fixed by #317, and #317 itself notes that the regression test for it would land here. This PR adds that scaffold. ## What's covered | File | Cases | Notes | |---|---|---| | \`test/screens/kyc/cubits/kyc/kyc_cubit_test.dart\` | 19 | Every \`KycCubit\` state transition: email auto-registration + recursion guard, legalDisclaimer / BitBox gates, AccountMergeRequested, KycPending, _continueKyc → KycUnsupportedStepFailure, KycCompleted, returning-user-must-sign, TFA via \`statusCode == 403\` *or* \`code == 'TFA_REQUIRED'\`, unrelated ApiException / generic exception → KycFailure, custom \`requiredLevel\`, and the cross-call generation-counter regression for #315 / #317 | | \`test/screens/kyc/steps/registration/cubits/registration_submit/kyc_registration_submit_cubit_test.dart\` | 8 | Happy path, BitboxRequired, the \`ApiException\`-after-sign blanket-swallow path (account-exists / merge), generic post-sign failure (\`Signature was empty\` / network / parse), missing-mail, getUser failure, retrySubmit success + still-disconnected | | \`test/packages/service/dfx/dfx_auth_service_test.dart\` | 6 | \`getSignature\` cache hit / address mismatch / fresh sign + cache / empty + \`'0x'\` guards, \`getAuthToken\` cache short-circuit. **Gap:** 3 min sign timeout — needs \`fake_async\`, called out as a TODO | | \`test/packages/wallet/eip712_signer_test.dart\` | +2 | Empty-signature and \`'0x'\` guards for the BitBox cancel / BLE-disconnect paths | Stack: \`bloc_test\` + \`mocktail\` (already in \`dev_dependencies\`). Test layout mirrors \`lib/\` per CONTRIBUTING.md. ## #315 / #317 regression test The retry-race test resolves the assertion that #317's PR description deferred to #314: > Unit test for the retry-race scenario will be added in #314 Phase 0, once the \`kyc_cubit_test.dart\` scaffold lands. It hangs call 1's \`getKycStatus\`, fires call 2 against a fresh response, then resolves call 1's late response and asserts (a) the listener never sees a state after \`KycCompleted\` and (b) \`cubit.state == KycCompleted()\` — i.e. the generation guard kills the stale leak. ## Test plan - [x] \`flutter analyze\` — clean (only the pre-existing \`i18n.dart\` generated-code warning) - [x] \`flutter test\` — **223 / 223 passing** locally (up from 188; +35 net new cases) - [x] \`dart format\` — applied - [x] No production-code changes; only new tests + an extension to the existing \`eip712_signer_test.dart\`
## Summary Stage 4 of the coverage push. Adds bloc_test specs for five screen-level cubits/blocs that previously only had widget-level coverage. All run in pure Dart (no widget pumping); mocktail for the service / repository / SoftwareWallet boundaries. | Cubit | Test file | Cases | | --- | --- | --- | | \`legal_disclaimer_cubit.dart\` | \`test/screens/legal/cubit/legal_disclaimer_cubit_test.dart\` | 7 | | \`restore_wallet/cubit/validate_seed/validate_seed_cubit.dart\` | \`test/screens/restore_wallet/cubit/validate_seed_cubit_test.dart\` | 7 | | \`transaction_history/cubits/filter/transaction_history_filter_cubit.dart\` | \`test/screens/transaction_history/cubits/transaction_history_filter_cubit_test.dart\` | 6 | | \`verify_seed/cubit/verify_seed_cubit.dart\` | \`test/screens/verify_seed/cubit/verify_seed_cubit_test.dart\` | 6 | | \`pin/bloc/setup_pin/setup_pin_cubit.dart\` | \`test/screens/pin/setup_pin_cubit_test.dart\` | 11 | ## What each file covers - **legal_disclaimer_cubit:** initial step-0 state, nextStep advance, full walk to last step, onComplete callback fires on last step (no emit), no-op without callback on last step, previousStep moves back, no-op at step 0. - **validate_seed_cubit:** initial uncomplete; \`checkSeedLength\` for 12 valid words / fewer-than-12 / 12 words with an out-of-wordlist token / extra inner whitespace tolerated; \`validateSeed\` delegates to \`WalletService\` for both branches. - **transaction_history_filter_cubit:** subscribes to the repo stream with the configured asset + address, default 1-year-back \`startDate\`, stream pushes populate \`all\` + \`filtered\`, date-window filter narrows \`filtered\` without touching \`all\`, boundaries are inclusive, filter re-applies on subsequent stream emissions. - **verify_seed_cubit:** 4 random ascending word indices within seed length, debug-mode pre-fill, \`canVerify\` reflects all four slots filled, \`updateWord\` trims + lowercases + clears \`hasError\`, \`verify\` returns true and marks the wallet current on match, \`verify\` returns false / flags \`hasError\` / does NOT mark current on mismatch. - **setup_pin_cubit:** initial state, \`addDigit\` appends / 6-digit cap, \`deleteDigit\` / no-op on empty, create→confirm transition on 6th digit, matching confirm-pin persists salt + hash and emits \`isComplete\` (exercises real PBKDF2 via \`compute()\`), mismatching confirm-pin resets + flags \`mismatch\`, \`reset\` returns to initial, \`isBiometricAvailable\` + \`enableBiometrics\` passthrough to \`BiometricService\`. ## Notes - The \`setup_pin_cubit\` matching-pin test runs a real **600k-iteration PBKDF2** through \`compute()\`. On the Flutter-test isolate shim this takes ~12 s on a Mac Studio. The test uses a 30 s timeout to keep CI honest without flaking. - \`SecureStorage.setPinSalt\` takes a \`Uint8List\`, which is a restricted type — a \`Fake\` subclass is illegal, so the mocktail fallback is registered with a real \`Uint8List(0)\` instance instead. ## Excluded (and why) - \`pin/bloc/verify_pin/verify_pin_cubit.dart\` — talks to \`SecureStorage.verifyPin\` which already exercises PBKDF2. Adding a full happy-path test would compound the slow-test cost. Will be covered in a follow-up. - \`restore_wallet/cubit/restore_wallet_cubit.dart\` — coupled to \`WalletService.restoreWallet\` (already covered in \`wallet_service_test.dart\`) plus screen navigation; small marginal value here, fits better in a Tier 1 integration test. - Buy / sell / KYC cubits — owned by other recent PRs (#312/#319/#321 area) and would invite merge conflicts. - Bitbox-coupled cubits (\`sell_bitbox\`, \`hardware_connect_bitbox\`) — belong with Tier 1 \`FakeBitboxCredentials\` (now landed via #319/#320) in a follow-up. ## Test plan - [x] \`flutter analyze test/screens/\` clean - [x] \`flutter test\` on all 5 new files — 37 / 37 passing (~8 s total locally) - [ ] CI green
…res (#466) ## Summary Fixes two related bugs in the KYC merge flow for existing DFX customers plus a missing validator in the registration form. Closes #464, refs #465. - **Bug A (#464)** — `KycCubit._registrationSignProduced` (formerly `_bitboxConfirmed`) was never marked after a successful DFX-merge email confirmation, even though the merge path produces the same EIP-712 signature as the new-registration path. After accepting the disclaimer, the user was routed to an empty personal-data form for data already on file at DFX. Submit would have produced a fresh EIP-712 sign over user-typed data, attaching potentially-divergent values (diacritics, address abbreviations, phone format) to the existing DFX account. - **Bug B (#465)** — `KycEmailVerificationCubit.checkEmailVerification` emitted `Success` unconditionally after `_completeRegistration`, silently swallowing both the null-userData case (likely a backend propagation race) and `registerWallet` throws. The user perceived the merge as successful while the wallet was not actually signed and registered. - **Address-number validator** — `KycRegistrationAddressStep.addressNumberCtrl` had no validator at all; empty values would pass form validation and reach the backend. Mirrors the validator pattern used by the other fields in the same step. ## Commits - `3d98c8d` — `fix(kyc): close existing-customer merge misroute, rename sign gate flag` - `e71f71e` — `fix(kyc-email-verification): surface failures, add generation guard + retry path` - `4d44c21` — `fix(kyc): require street number in registration address form` ## What changed ### Bug A - Rename `_bitboxConfirmed` → `_registrationSignProduced`, `markBitboxConfirmed()` → `markRegistrationSignProduced()`. The flag is wallet-agnostic — silent local sign for software wallets, 13-step ceremony for BitBox. The old name routinely misled readers. - Call `markRegistrationSignProduced()` from `kyc_email_page.dart` on a successful merge confirmation pop, before re-running `checkKyc`. Without this, the existing-DFX-customer flow would still hit the gate after the disclaimer step. - Update the doc comment around the gate to focus on per-session sign semantics rather than the hardware-wallet UX surface. - Fix `docs/testing.md:56` — the code example referenced the renamed method (would not compile if copied). - Update `kyc_cubit_test.dart` references and group name. - Add widget tests in `kyc_email_page_test.dart` that verify the setter is called when the merge-confirmation pop returns `true` and is **not** called on `false` / `null`. Without these a future refactor could remove the setter call again without any test failing. ### Bug B - `_completeRegistration` now returns `Future<bool>` indicating whether the wallet was actually registered. - `checkEmailVerification` only emits `Success` when `_completeRegistration` returned `true`; on failure the cubit stays in `RegistrationFailure` so the verification page stays open and the user can retry. - Add `_runGeneration` cancellation-token (mirrors `KycCubit`): guards every emit against stale calls and against `isClosed`, prevents a late HTTP response from emitting after a retry, and lets a fast double-tap supersede the in-flight call instead of racing two emit sequences. - Add `_mergeDetected` short-circuit so the retry path actually retries. Once the JWT account-id change has confirmed the merge, the auth side is settled — a retry after a null-userData race should not re-run the account-id comparison (which would emit `Failure` because `getAuthToken` keeps returning the merged account). Subsequent retries skip the auth check and go straight to `_completeRegistration`, where the propagation race usually resolves. - Rewrite the misleading `registerEmailVerificationRegistrationFailed` i18n strings (DE + EN). Old text claimed the wallet had been assigned to the account — only true on the `registerWallet` throw path, not on the null-userData branch. New text is neutral and actionable for both failure modes. - Update the two existing tests that asserted the silent-Success behaviour to assert the new, correct behaviour: failures surface to the UI. - Add a new test for the retry path: two consecutive calls with a null-userData first response and a populated second response end in Success. ### Address-number validator - Add an empty/null-rejecting validator on `addressNumberCtrl` in `kyc_registration_address_step.dart`, matching the existing validators on street, postal code, city, country. ## What did **not** change - The per-cubit-instance security model is preserved: every `/kyc` entry still constructs a fresh `KycCubit` and the gate still requires a fresh sign before sensitive steps. The fix encodes a fact the merge path was already producing, it does not loosen the gate. - For users who legitimately hit `KycStep.registration` (genuinely new users without DFX data) the flow is identical. - The page-level listener in `kyc_email_verification_page.dart` already handled all three states (`Failure`, `RegistrationFailure`, `Success`) correctly — Failure shows a snackbar and stays open, Success pops with `true`. No changes needed. - Backend contracts: untouched. ## Local verification - `flutter analyze` — clean (`No issues found! (ran in 1.6s)`) - `flutter test` — **1398 / 1398 passed** (1395 develop baseline + 3 new tests) ## Test plan - [ ] **Manual on Android** — existing DFX customer flow (the reproduction from #464): - [ ] dashboard → Buy → Next → enter DFX email → confirm via email link → tap "I've confirmed" → accept disclaimer wizard (5 steps) → **lands on dashboard / next KYC step, not on the empty form** - [ ] cancel mid-flow → return → flow remains coherent - [ ] **Manual on Android** — new user (non-DFX) flow: - [ ] dashboard → Buy → Next → enter fresh email → continue → fill registration form → submit → flow completes - [ ] try submitting with empty street-number field → blocked by validator - [ ] **Manual on Android** — Bug B regression check: - [ ] If reachable in test env, simulate a `registerWallet` failure (network kill, backend 5xx) → user sees the snackbar and the verification page stays open → user can retry - [ ] If a null-userData propagation race can be induced, verify the second tap on "I've confirmed" goes through ## Items deliberately deferred to a separate PR Surfaced during review of #466 but kept out-of-scope to keep this PR coherent: - Re-frame Bug A commit-message wording from "security" to "UX/data integrity" — already reflected in the rewritten commit message of `3d98c8d`. Bug B's mechanism is the more severe of the two (wallet not registered presented as success); Bug A is misroute + data-integrity risk via form submit. - Backend idempotency for `/register/wallet` when the wallet is already registered — out of scope for the app repo. ## Related - Closes #464 - Refs #465 - Touches code introduced by PR #312 (`fix: gate sensitive KYC steps behind BitBox EIP-712 sign`) — the merge case appears to have been overlooked at that time - Compatible with PR #317 (`fix(kyc): prevent stale state leak in KycCubit on retry`) — extends the same `_runGeneration` pattern to `KycEmailVerificationCubit`
Important
Merge-blocked on DFXswiss/bitbox_flutter#11. Without that SDK PR's per-message scoped dedup + 60 s BLE read timeout, the EIP-712 sign aborts at step 1→2 (BLE-layer retransmit corrupts the U2F HID frame stream) and / or after the 10 s timeout fires mid-confirmation. Sequence: merge #11 → tag
v0.0.3→ bump this PR'spubspec.yamlfromv0.0.2tov0.0.3→ merge here.Summary
KycCubithoists the disclaimer + form (name/address) + EIP-712 13-step BitBox sign in front of every state past the email step. Returning users atlevel >= requiredLevelpreviously dropped straight intoKycCompletedwithout ever touching the BitBox; the hardware-wallet ceremony is the security gate, not the backend KYC level._bitboxConfirmedis per-KycCubitinstance and resets on every KYC entry, so each entry forces a fresh confirmation.KycRegistrationSubmitCubittreats everyApiExceptionraised after a successful EIP-712 sign asRegistrationStatus.completed. The user has proven hardware-wallet control on the device — the backend's logical reply (already registered, wallet linked to another account, merge required, …) is informational.KycCubit.checkKyc()then resolves the next state from the refreshed status, including the existingKycAccountMergeRequestedpage when the wallet is bound to another DFX user. Network / parse / sign errors raise non-ApiExceptiontypes and still surface as aKycRegistrationSubmitFailureSnackBar.Eip712Signer._signTypedDatanow throws when the signature comes back empty or'0x'. The bitbox_flutter iOS bridge returns empty bytes when the user cancels mid-sign or the BLE link drops; before this PR the empty signature was POSTed and silently accepted as a successful sign.statusCode == 403andcode == 'TFA_REQUIRED'as the trigger for routing to the 2FA step (paired with fix: ApiException null statusCode crash on 2FA #310'shttpStatusCodefallback so the code path is reachable).checkKycis wrapped in a 30 s top-level timeout. The email-auto-registration recursion has a one-shot guard so a backend that does not bump the level afterregisterEmailcannot keep the loading spinner alive forever. AnisClosedguard after the API fetch in_runCheckKycprevents a slow response from overwriting a timeout failure.DFXAuthService: removed the BitBox auth skip from fix: gate DFX auth at the source instead of HomeBloc only #304 (no longer needed after the v0.0.2 SDK fixes — see inline comment for context). Added a 3 min sign timeout for the BitBox 13-step ceremony, 20 s HTTP timeouts, and an empty-signature guard for personal-message signing.registerEmailVerificationBitboxSignHint) below the confirm button while the signature ceremony is running.Test plan
flutter analyze— no issuesflutter test— 188 tests passKycAccountMergePage("Identität in anderem Konto gefunden, Merge per Email bestätigen") instead of the generic failure screenlevel >= 30: disclaimer + form + BitBox sign first, thenKycCompleted(no silent grant)Signature was emptySnackBar, no silent successcode: TFA_REQUIRED→ cubit emitsKycSuccess(twoFa)instead ofKycFailure