Promote: staging -> develop - #888
Merged
Merged
Conversation
## Summary Soft launch for the two new phase-2 features: the **Pay** (OpenCryptoPay, #674) and **Send** (W2W transfer, #687) dashboard actions are hidden behind an invisible wall so only insiders can reach them. - The two dashboard buttons render only when a persisted `insiderFeaturesUnlocked` flag is set; Buy and Sell stay visible unconditionally. - Unlock: tap the version number in Settings **seven times** (developer-options pattern). A snackbar confirms the unlock; the flag persists across restarts (SharedPreferences, seeded into `SettingsBloc`). - The OpenCryptoPay payment deeplink intentionally keeps working regardless of the unlock state, so payment links handed to insiders resolve as before. ## Deliberate deviation from the API-authority rule (reviewed, intentional) CONTRIBUTING lists "feature visibility based on local state" as not OK and prefers an API capability flag. This gate deviates from that on purpose, as a product decision made with the API-capability alternative on the table: - The point of the soft launch is that outsiders must not even *see* the features, and the unlock must work offline/instantly for anyone told the gesture — an account-bound API capability would change the product (server-side insider bookkeeping, no gesture unlock). - No API truth is duplicated or contradicted: there is no server-side notion of this soft launch, and the API remains the sole decision authority for every actual transfer/payment the flows perform. The unlocked app renders exactly what the API-authorized app rendered before this PR; the locked app renders a subset. - Being a public repo, the mechanism is readable in source — the wall is a discoverability hurdle, not a security boundary. ## Implementation notes - The Settings version row keeps its exact visuals; it moves into a new `SettingsVersionUnlock` widget that follows the page-local `getIt` bloc-access pattern (its test harness deliberately mirrors the settings golden harness structure). - `DashboardActions` gates Pay/Send with collection-`if`s on `context.watch<SettingsBloc>()`. - `ActionButton` now scales its icon/label column down (`FittedBox`) instead of overflowing its fixed 110x50 box — the new actions matrix exposed real overflows under Expanded width squeeze (German labels, 2px at 1.0x on narrow devices) and at large text scales (up to 258px at 3.0x). The tap area stays the full box (the `InkWell` wraps it, not the scaled content). Layouts that fit are visually unchanged; the four positive-balance dashboard goldens picked up sub-pixel antialiasing deltas (77-107 bytes each) from the new render path and were regenerated by the runner. - `expectFullyTappable` maps both rect corners through the render transform, so scaled targets measure their visual rect (transform-neutral for every existing call site). - The repository setter follows the established fire-and-forget persistence idiom; the repo-wide hardening idea is tracked in #886. Pre-existing positive-balance dashboard overflows (CashHoldingBox and siblings) are tracked in #887 and deliberately not part of this PR. ## Handbook Section 79 of the handbook (/de/#insider-unlock) explains the unlock step by step in German with three screenshots (settings version row, dashboard before, dashboard after) so the link can be shared directly with the people who should know. The new dashboard_insider_unlocked golden is mapped as handbook screenshot slot 269; the three updated dashboard baselines were already mapped and refresh automatically on the next handbook deploy. ## Tests - New widget tests for the 7-tap unlock (6 taps inert, 7th dispatches exactly one event + snackbar, 9 rapid taps still dispatch exactly once, taps ignored once unlocked, version text still rendered) - `DashboardActions` locked/unlocked cases incl. the existing navigation assertions, plus a locked→unlocked transition test that pins the `context.watch` rebuild behaviour - New golden case `dashboard_insider_unlocked` (renders the same four-button dashboard the pre-PR baseline showed); existing dashboard goldens change to the 2-button locked default - New responsive-matrix group renders `DashboardActions` standalone (insider unlocked, all four buttons) across the full device/text-scale grid with overflow + tappability gates — scoped to the actions row this PR owns; the zero-balance page matrix is unchanged - Repository getter/setter covered against a real SharedPreferences backend (100% lines floor on `lib/packages/*`) - Full suite on the verification host: 4755 tests green, analyzer clean --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Removes the KYC onboarding dead ends: adds the missing `PersonalData` step page, and replaces the generic "cannot be completed in this app" failure screen with an actionable handoff. Covers items 4a and 5 of DFXswiss/api#4556, and the dead-end class first flagged as K1/K5 in #613. ## Problem Registration satisfies `PersonalData` without the user ever seeing it, so the app has never had a page for it. But the step **re-opens** when identification rejects the submitted data as not matching the document: the API fails the completed step and opens a fresh one *specifically so the account can correct it*. The app could not render that step. Instead of a correction form the user got the unsupported-step failure screen — "The current KYC step (PersonalData) cannot be completed in this app" — and onboarding dead-ended with no way forward and no way back. It cannot self-heal: the open step blocks the API from opening any later one. This is not a corner case. It is the standard identification data-mismatch flow, and it recurs every time a submitted name or address does not match the document. ## Change - `KycStep.personalData` + the `_mapStepName` arm, so the step routes to a page instead of the failure screen. - `KycPersonalDataPage` — first/last name, phone, street, house number, postcode, city, country. Same field widgets, validators and layout as the registration address/personal steps. - `KycPersonalDataCubit` submits through the generic `setData` PUT already used by the nationality and settings address/name flows, building the body from the existing `KycPersonalData`/`KycAddress` models. ## Guards, and why each exists - **Never offered to a non-personal account.** Submitting this form sets `accountType`, and the API explicitly nulls all six organization columns whenever that value is `Personal` (`user-data.service.ts`, the `isPersonalAccount` branch) and drops five org-only steps from `requiredKycSteps`. The page reads the account type from the registration payload and refuses to render for anything but personal; the cubit sends exactly the value the page gated on, so the two cannot drift. - **Seeded, not blank.** The copy asks the user to check their details and every submit rewrites all eight fields, so an empty form would force a from-memory re-entry in which a typo silently overwrites data that was already correct. - **A missing payload gets a retry, not a dead end** — mirrors `KycLinkWalletPage`'s defensive refresh surface, and has its own golden. - **A late country lookup never overwrites a country the user already picked.** The page and `CountryField` issue independent `GET /v1/country`s and the service does not de-dupe in-flight calls, so either can win. ## The generic dead end `_mapStepName` renders 6 of the 24 step names the API can return. Every other one produced `KycUnsupportedStepFailure`, which rendered the generic failure page — `actions: const []`, a true dead end — with the step's raw wire identifier printed into the message. The user had nothing to do and nothing useful to tell support. `KycUnsupportedStepPage` replaces it with a retry and a route to support, and names no step. The retry is not decorative: for a step under internal review or one the API advances by itself, re-reading is the only way the user finds out. For a genuinely unrenderable step it re-emits the same state, which is honest — the copy and the support CTA are what move that case forward. The identifier is gone deliberately. It is an internal enum value, and DFX support reads the same step server-side, so nothing diagnosable was lost. The personal-data organization refusal renders this same page, so there is one answer to "this step cannot be shown here" instead of two. This covers `Recommendation`, `ResidencePermit` and every future step name at once. Prod data shows no RealUnit account currently behind `ResidencePermit`, and only a very small latent population behind `Recommendation`, so dedicated forms for them would have been the more expensive way to fix less. ## Shared-widget fix `PhoneNumberField` left `prefix` null whenever a seeded value did not start with a dial code it offers. Its prefix dropdown carries no validator, so `Form.validate()` returned true while `updatePhoneNumber()` silently refused to write — the stale number was submitted instead of what the user typed. It now falls back to the first prefix; the number field starts empty, so the validator still blocks submit until it is re-entered. This also fixes the registration prefill (`kyc_registration_page.dart`), which seeds `dto.phoneNumber` unconditionally and is the path that provably carries arbitrary dial codes. ## Verification - `flutter analyze` — no issues. `flutter test --exclude-tags golden` — **4723 passed**. - 4 cubit tests, 17 widget tests, 3 goldens, a `kyc_page_manager` case pinning both hops of the payload plumbing, and both new sticky-CTA surfaces registered in the responsive catalog with full device × text-scale matrix coverage. - Every guard mutation-checked: dropping the account-type gate, the retry branch, the prefill, the url plumbing, either plumbing hop, the country-lookup catch, the racing-pick guard, the `PhoneNumberField` fallback, the handoff page, its retry, its support route, or moving the support CTA out of the sticky block each turns the suite red. - Toolchain matched the CI pin (Flutter 3.41.6); golden baselines produced by `golden-regenerate.yaml` on the self-hosted runner, never locally. The last regeneration after the shared-widget change committed **no** baseline, confirming it is behaviour-neutral for every state under test. ## Two existing tests were updated, not worked around `kyc_cubit_test.dart` used `personalData` as its stand-in for "a step name with no UI mapping" — that premise is now false, so it uses `statutes`. `kyc_bitbox_create_wallet_states_test.dart` pinned the `KycStep` enum at ten variants; it now pins eleven. ## Not in scope Reporting an unmapped step to telemetry. The app has no runtime SDK — `sentry_dart_plugin` is a dev-dependency that only uploads symbols, and there is no `Sentry.capture*` anywhere in `lib/`. Adding one is a product decision, not something to fold in here. Worth doing: this class of failure is invisible in monitoring today, because every response involved is a 200. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…TRY_DSN gate (#878) ## Problem Two customer cases this week reached support with **zero diagnostic evidence**: a BitBox02 USB connect crash/hang on Android and a registration-signing blocker (both 23.07.). Server-side request tracing covers everything that reaches the API — but a client-side crash before any request is invisible. The app has no crash reporter; every error thrown from a `Future`, `Stream` or `Timer` callback vanishes without trace (#866 documents this gap explicitly, and `installErrorHandlers()` was designed as the hook a reporter plugs into). ## Change - **`sentry_flutter` behind a compile-time gate.** `initCrashReporting()` (new, `lib/setup/error_handling/crash_reporting.dart`) reads `--dart-define=SENTRY_DSN`. Without an injected DSN — i.e. in every local, test and current CI build — the SDK never starts and produces no network traffic. The app behaves exactly as before this PR. - **Best-effort by contract:** a malformed DSN or a failing native binding is logged and swallowed inside `initCrashReporting()` — reporting infrastructure can never keep the wallet from starting. Covered by a dedicated test. - **Wiring order is load-bearing:** `main()` calls `initCrashReporting()` *after* `installErrorHandlers()`, because `installErrorHandlers()` overwrites `PlatformDispatcher.onError` without chaining — the reverse order would silently drop the SDK's async-error hook. The SDK itself chains both handlers it wraps (`FlutterError.onError`: capture, then delegate; `PlatformDispatcher.onError`: delegate, then capture). In release mode the native splash is now preserved *before* this first await, so the added async gap cannot flash the splash to blank. - **Pinned option surface** — the guarantee is exactly this list: `sendDefaultPii=false`, `attachScreenshot=false`, `enableAutoSessionTracking=false`, `tracesSampleRate=null` — error events only, no session telemetry. Native crash handling and ANR detection deliberately stay on their SDK defaults: they produce precisely the error events this reporter exists for. View-hierarchy attachment stays off by SDK default; its option is experimental and deliberately not referenced. Environment defaults to `production`, overridable via `--dart-define=SENTRY_ENVIRONMENT`. - **CONTRIBUTING § API Access:** adds the one scoped exception for first-party crash reporting, and declares any widening of the reported data (breadcrumbs with request URLs, user context, attachments) a review-blocking change. ## Tests - `test/setup/error_handling/crash_reporting_test.dart`: DSN gate (no-op without DSN, exactly one init with DSN), the full pinned option set, and the swallow-on-failure contract, via the injectable `CrashReporterInit` — no platform channels involved. - `// @no-integration-test` annotation on `initCrashReporting` per CONTRIBUTING: the native SDK only starts in builds that inject a DSN, which no test build does. ## Open points before this becomes effective 1. **Release pipeline:** the release workflow must inject `SENTRY_DSN` (repo/environment secret + `--dart-define`) — deliberately a separate PR, so this one stays inert and fully reviewable on its own. 2. **Native release build check:** PR CI runs analyze + tests; the first tagged build after the pipeline change should be smoke-checked once (Android Gradle / iOS pods pull in the native SDK parts). 3. **Follow-up (separate PR):** attach BitBox device context (product, firmware version) and connect-flow breadcrumbs after connect, so hardware-related crashes carry the device facts that today require asking the customer.
## Summary Follow-up to [#746](#746). That PR relabelled the Settings wallet action to **"Reset wallet" / "Wallet zurücksetzen"** and switched the confirm button to the existing `reset` key. The generic `logout` key — whose only consumer was that button — is now unreferenced. ## Change - Remove `"logout"` from `assets/languages/strings_de.arb` ("Abmelden") and `strings_en.arb` ("Logout"). ## Verification - `S.of(context).logout` has **0 call sites** across `lib/` and `test/` (`isLogout` in `settings_page.dart` is an unrelated local bool, not the key). - ARB stays valid; DE/EN key parity preserved (358 = 358). - No widget renders the string, so **no goldens change** and `Visual Regression` is unaffected. ## Test plan - [ ] `Analyze & Test` green - [ ] `Visual Regression` green (no baseline change expected) - [ ] `Coverage Floor Gate` green
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automatic Staging PR
This PR was automatically created after changes were pushed to staging.
Commits: 1 new commit(s)
Checklist