Skip to content

Promote: staging -> develop - #888

Merged
TaprootFreak merged 4 commits into
developfrom
staging
Aug 3, 2026
Merged

Promote: staging -> develop#888
TaprootFreak merged 4 commits into
developfrom
staging

Conversation

@github-actions

@github-actions github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Automatic Staging PR

This PR was automatically created after changes were pushed to staging.

Commits: 1 new commit(s)

Checklist

  • Review all changes
  • Verify CI passes
  • Approve and merge to promote into develop

TaprootFreak and others added 4 commits August 3, 2026 22:54
## 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
@TaprootFreak TaprootFreak added the tier3:full Opt-in: run Tier 3 Maestro handbook flows on this PR label Aug 3, 2026
@TaprootFreak
TaprootFreak merged commit bb855f4 into develop Aug 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tier3:full Opt-in: run Tier 3 Maestro handbook flows on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants