Release: develop -> main#325
Open
github-actions[bot] wants to merge 233 commits into
Open
Conversation
) ## Summary Adds 38 unit tests across six previously-untested files in `lib/packages/utils/` and `lib/packages/wallet/`. Each spec lives in the mirror path under `test/` per the project convention. | File under test | Test file | Cases | | --- | --- | --- | | `lib/packages/utils/fast_hash.dart` | `test/packages/utils/fast_hash_test.dart` | 5 | | `lib/packages/utils/jwt_decoder.dart` | `test/packages/utils/jwt_decoder_test.dart` | 7 | | `lib/packages/wallet/payment_uri.dart` | `test/packages/wallet/payment_uri_test.dart` | 4 | | `lib/packages/wallet/wallet_account.dart` | `test/packages/wallet/wallet_account_test.dart` | 7 | | `lib/packages/wallet/wallet.dart` | `test/packages/wallet/wallet_test.dart` | 11 | | `lib/packages/wallet/eip7702_signer.dart` | `test/packages/wallet/eip7702_signer_test.dart` | 4 | ## What each file covers - **fast_hash:** FNV-1a determinism, distinct inputs differ, ordering matters, unicode safety, empty-string offset basis. - **jwt_decoder:** well-formed payload parsing, segment-count errors, non-map payload rejection, all three valid base64url padding lengths, illegal length-mod-4 rejection. - **payment_uri:** empty-amount short form, dotted and comma-locale amounts, preservation of decimal precision. - **wallet_account:** BIP-44 derivation path format, deterministic Hardhat account #0 address from the standard test mnemonic, distinct indices, signMessage shape + determinism + sensitivity to addressIndex. - **wallet:** `SoftwareWallet` walletType + primary/current account identity + `selectAccount` semantics + id/name mutability; `DebugWallet` sign refusal. - **eip7702_signer:** hardware-credential refusal (BitBox cannot sign EIP-7702), software-credential signing, signature determinism, nonce-sensitivity. ## Why Stage 2 of the coverage push tracked in the README features matrix ([#322](#322)). These six files are pure-Dart with no platform dependencies, so they were the cheapest meaningful coverage win available — no service mocks, no widget rendering, no merge-conflict risk with the three in-flight test PRs ([#319](#319), [#320](#320), [#321](#321)). ## Test plan - [x] `flutter analyze` clean on all six new test files (locally on Flutter 3.38.5) - [x] `flutter test test/packages/utils/ test/packages/wallet/` — 38 / 38 passing - [ ] CI green on this branch - [ ] Spot-check: coverage artifact from #323 (once that lands) shows non-zero coverage on the six files above
## Summary Replace the bare README with a full project-overview document modelled on the `zk-coins/app` README. Adds: 1. **Contributing rule:** 100% test coverage on the activated surface is required to merge into `develop`. Defensive code is exempted via `// coverage:ignore-*`. Branch protection enforces. 2. **Coverage scope:** `lib/packages/**` + every `cubits/`/`bloc/` directory under `lib/screens/<feature>/`. Widget files render-tested via `testWidgets` only. 3. **Coverage infrastructure roadmap:** honest list of what still needs to land before the rule is enforceable (CI `--coverage` step, lcov threshold gate, branch protection, build-time feature flags, inline ignore annotations). 4. **Features matrix:** every user-facing function, its activation status, the triage decision (`mvp` / `defer` / `planned`), and the tests that currently cover it. 5. **Triage gaps:** explicit list of `mvp` features still below 100%, with links to the in-flight PRs ([#319](#319), [#320](#320), [#321](#321)) that close them. 6. **Testing tiers:** ties the matrix to the 4-tier model in [#314](#314). 7. Original **Getting started** section preserved at the end. ## Why The repo has no source of truth for "what does this wallet actually do" or "what is its test-coverage commitment". Tests in `test/` exist but are unbound to features — a reviewer cannot tell at a glance whether a feature added in a PR has the required coverage. This document closes that gap. The coverage rule deliberately mirrors `zk-coins/app` exactly (same wording, same scope shape) so that reviewers familiar with one project transfer their expectations to the other. The 4-tier testing model is owned by [#314](#314); this README references it but does not duplicate it. ## What this PR is _not_ - It does **not** wire `flutter test --coverage` into CI. That is item 1 of the roadmap and will land in a follow-up. - It does **not** add branch protection. That is item 3 of the roadmap. - It does **not** introduce build-time feature flags. That is item 4 of the roadmap and is a prerequisite for the rule being realistic across all features. - It does **not** add any new tests — the in-flight test PRs ([#319](#319), [#320](#320), [#321](#321)) own that work. Until the roadmap items land, the rule is aspirational, explicitly noted as such in the README, and not yet a merge blocker. ## Test plan - [ ] Visual review of the rendered README on GitHub (tables align, all links resolve) - [ ] Confirm every `widget`-tagged row points at an existing `*_test.dart` file in `test/` - [ ] Confirm every "in flight" reference points at an open PR - [ ] Confirm "Coverage scope" wording matches what we want to measure once the lcov gate lands
## Summary Adds `flutter test --coverage` to the existing PR workflow and uploads the (filtered) `lcov.info` as a CI artifact. - `flutter test --coverage` replaces the plain `flutter test` invocation - `coverage/lcov.info` is filtered with `lcov --remove` to drop generated files (`lib/generated/**`) and `lib/main.dart` so the artifact reflects only the activated surface - The filtered report is uploaded as artifact `coverage-lcov` on every run (`if: always()`) - `lcov --summary` prints to the CI log so contributors can eyeball the line/function/branch numbers without downloading the artifact ## Why Step 1 of the coverage infrastructure roadmap documented in PR #322. Without measurement, the 100% rule is unverifiable; with measurement but no threshold, this PR is safe to merge today and lays the foundation for the threshold gate in a follow-up. ## What this PR is _not_ - Does **not** enforce any threshold. The build still passes regardless of coverage %. Hard-gating below 100% would block every open PR today (current coverage ratio ≈ 10%). - Does **not** add any new tests. - Does **not** change which files exist in `test/`. ## Test plan - [ ] CI run completes green - [ ] `coverage-lcov` artifact is present on the PR's checks page - [ ] `lcov --summary` line of the CI log shows non-zero coverage on `lib/packages/**` - [ ] Filtered report excludes `lib/generated/**`
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 3 of the coverage push. Adds 38 unit tests across four previously-untested service files in \`lib/packages/service/\` that have no DFX backend coupling. \`mocktail\` mocks for the repository / bitbox dependencies; no real I/O. | Service | Test file | Cases | | --- | --- | --- | | \`session_cache.dart\` | \`test/packages/service/session_cache_test.dart\` | 9 | | \`settings_service.dart\` | \`test/packages/service/settings_service_test.dart\` | 6 | | \`app_store.dart\` | \`test/packages/service/app_store_test.dart\` | 6 | | \`wallet_service.dart\` | \`test/packages/service/wallet_service_test.dart\` | 17 | ## What each file covers - **session_cache:** auth-token in-memory-only lifecycle (no disk write); signature/address persistence through \`CacheRepository\`; \`loadSignature\` preserves in-memory state when already set; tolerates missing repo entries; \`clear\` wipes both keys, auth token, and memory. - **settings_service:** terms / software-terms read + write delegation; the two flags are independent. - **app_store:** wallet-not-set throws; primaryAddress derived through current account; selectAccount-driven address change; \`apiConfig\` getter re-evaluates on every access (network mode can switch at runtime); sessionCache passthrough. - **wallet_service:** \`createSeedWallet\` (bip39 generation + persistence, does NOT mark current); \`restoreWallet\` (seed persistence + marks current); \`createDebugWallet\` (view-wallet path); \`getWalletById\` (software / debug branches + null-repo error); \`setCurrentWallet\`; \`getCurrentWallet\` (resolves through id); \`deleteCurrentWallet\`; \`hasWallet\` (both branches); \`validateSeed\` (valid / invalid / empty / wrong-checksum). ## Excluded (with reasons) - **biometric_service.dart** — the \`LocalAuthentication\` instance is constructed in the field initializer with no seam. Testing requires either a prod refactor (out of scope here) or platform-channel test plumbing. Flagged in the README features matrix as a triage gap. - **transaction_history_service.dart** — hits \`app_store.httpClient\` (a final \`http.Client\` field) which is not currently injectable. Same constraint as the DFX services. - **price_service.dart** — abstract interface only; no implementation to test yet. - **wallet_service \`createBitboxWallet\` / getWalletById bitbox branch** — belongs to Tier 1 with \`FakeBitboxCredentials\` (now landed via #319/#320). Will be covered in a follow-up. ## Test plan - [x] \`flutter analyze\` clean (locally on Flutter 3.38.5) - [x] \`flutter test test/packages/service/\` — 38 / 38 passing on these new files - [ ] CI green
## 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
## Summary Stage 5 of the coverage push. Adds 32 unit tests for the PIN unlock cubit and three previously-untested DFX backend services. mocktail mocks for AppStore + repository boundaries; \`http.testing.MockClient\` for the HTTP surface. | File under test | Test file | Cases | | --- | --- | --- | | \`lib/screens/pin/bloc/verify_pin/verify_pin_cubit.dart\` | \`test/screens/pin/verify_pin_cubit_test.dart\` | 18 | | \`lib/packages/service/dfx/dfx_country_service.dart\` | \`test/packages/service/dfx/dfx_country_service_test.dart\` | 5 | | \`lib/packages/service/dfx/dfx_blockchain_api_service.dart\` | \`test/packages/service/dfx/dfx_blockchain_api_service_test.dart\` | 5 | | \`lib/packages/service/dfx/dfx_faucet_service.dart\` | \`test/packages/service/dfx/dfx_faucet_service_test.dart\` | 4 | ## What each file covers - **verify_pin_cubit:** initial state; \`addDigit\` / \`deleteDigit\` including no-ops at boundaries + while \`VerifyPinTemporarilyLocked\` / \`VerifyPinLocked\`; correct pin resets lockout and emits \`VerifyPinSuccess\` (real 600k-iter PBKDF2 via \`compute()\`); wrong-pin first-attempt path; \`enableLockout: false\` never persists attempts; 5th wrong attempt triggers a 1-minute temporary lockout; reaching \`permanentLockoutThreshold\` emits \`VerifyPinLocked\` and skips the temporary-lockout write; \`onLockExpired\` preserves \`failedAttempts\`; \`checkBiometricAvailability\` for the in-window / expired / threshold / success / fail / unavailable branches. - **dfx_country_service:** list mapping from the DTO + cache (single fetch even on repeated reads), non-200 throws, case-insensitive symbol lookup, unknown-symbol error, public \`cachedCountries\` field is populated post-fetch. - **dfx_blockchain_api_service:** POST shape (address + Bearer JWT + chain name + asset id), testnet flips \`blockchain\` to \`'Sepolia'\`, empty balances list returns \`0.0\`, 201 acceptance in addition to 200, \`ApiException\` on non-2xx with the JSON body. - **dfx_faucet_service:** POST + Bearer JWT to \`/v1/faucet\`, 201 acceptance, \`ApiException\` on non-2xx, **documents** the current behaviour of sending the literal string \`'Bearer null'\` when no auth token is set — useful as a regression marker if/when the service grows a guard. ## Notes - The verify-pin happy-path test exercises a real PBKDF2 (600k iterations) via \`compute()\`. On the Flutter-test isolate shim this takes a few seconds; the test uses a 30 s timeout, same approach as in PR #327's setup_pin test. - \`MockClient\` lets us stub \`AppStore.httpClient\` without giving the service its own real \`http.Client\`; the production code is unchanged. ## Excluded (and why) - \`dfx_kyc_service\`, \`real_unit_registration_service\` — overlap with the KYC cubits that #319 already covers; want to avoid a stack on top of that work mid-review. - \`real_unit_account_service\`, \`transaction_history_service\` — hit \`appStore.wallet.currentAccount.primaryAddress.address.hexEip55\`, which needs a real \`SoftwareWallet\` plumbed through the mock. Doable but more setup than the surface justifies; will be covered in a follow-up alongside hook tests. - \`dfx_brokerbot_service\`, \`real_unit_sell_payment_info_service\` — possibly touched by #321 (dashboard buy/sell auth). Hold to avoid review-time conflicts. - \`biometric_service\`, \`price_service\` — same constraints documented in #326. ## Test plan - [x] \`flutter analyze\` on the four new files — clean - [x] \`flutter test\` — 32 / 32 passing locally - [ ] CI green
…sts) (#329) ## Summary Stage 6 of the coverage push. Adds 23 unit tests for two more DFX services and two screen-level cubits whose only previous coverage was at the widget layer. | File under test | Test file | Cases | | --- | --- | --- | | \`lib/packages/service/dfx/dfx_bank_account_service.dart\` | \`test/packages/service/dfx/dfx_bank_account_service_test.dart\` | 7 | | \`lib/packages/service/dfx/dfx_price_service.dart\` | \`test/packages/service/dfx/dfx_price_service_test.dart\` | 8 | | \`lib/screens/settings_contact/cubit/settings_contact_cubit.dart\` | \`test/screens/settings_contact/settings_contact_cubit_test.dart\` | 4 | | \`lib/screens/support/cubits/support_tickets/support_tickets_cubit.dart\` | \`test/screens/support/cubits/support_tickets_cubit_test.dart\` | 4 | ## What each file covers - **dfx_bank_account_service:** \`getBankAccounts\` GET shape (path, JWT header, list mapping) + \`ApiException\` on non-2xx; \`createBankAccount\` POST body, optional-label omission, error path; \`updateBankAccount\` PUT to \`/v1/bankAccount/{id}\` with only the provided fields (also pins the \`isDefault → "default"\` wire mapping). - **dfx_price_service:** \`getPriceOfAsset\` CHF / EUR scaled by 100 (rappen / cents); \`getPriceChart\` maps each entry with asset, scaled price, and UTC time; \`getChfToEurRate\` returns \`eur / chf\` or \`0.0\` when chf == 0 (no division by zero); all three paths throw on non-200. - **settings_contact_cubit:** Success with \`emailSet: true\` / \`false\`, Failure on async-throw, and a manual \`init()\` call recovering from a transient failure. - **support_tickets_cubit:** Loaded with DTO → \`SupportIssue\` mapping, Loaded(empty), Error on async-throw, and a manual \`loadTickets()\` call recovering from a transient failure. ## Notes - Both cubits fire their work (\`init()\` / \`loadTickets()\`) in the constructor, so by the time a stream subscriber attaches the synchronous Loading emit has already passed on the broadcast stream. The tests therefore assert the **final state** via \`stream.firstWhere\` rather than the full sequence — a deliberate documented choice in the test file headers. - Mocked \`getUser\`/\`getTickets\` use \`thenAnswer((_) async => throw …)\` instead of \`thenThrow\` so the throw goes through a microtask boundary (otherwise the cubit's \`catch\` block emits Failure before any listener can hear it). - Service tests use \`http.testing.MockClient\` against \`AppStore.httpClient\`, same pattern as #328. ## Excluded (and why) - \`dfx_kyc_service\` — large surface (≈ 260 lines), heavy overlap with the KYC cubit logic already covered by #319; would invite review-time conflicts. - \`dfx_brokerbot_service\`, \`real_unit_sell_payment_info_service\` — likely touched by #321 (dashboard buy/sell auth). - \`transaction_history_service\`, \`real_unit_account_service\` — depend on \`AppStore.wallet.currentAccount\` plumbing; will be covered alongside the wallet-coupled hook tests in a follow-up. - \`support_chat_cubit\`, \`support_create_ticket_cubit\` — share \`DfxSupportService\` with this PR; held back to keep the diff focused. ## Test plan - [x] \`flutter analyze\` on the four new files — clean - [x] \`flutter test\` — 23 / 23 passing locally - [ ] CI green
## Summary
Stage 7 of the coverage push. Adds 34 unit tests for the remaining
support cubits and three settings-user-data edit cubits.
| Cubit under test | Test file | Cases |
| --- | --- | --- |
| \`support/cubits/support_chat/support_chat_cubit.dart\` |
\`test/screens/support/cubits/support_chat_cubit_test.dart\` | 6 |
|
\`support/cubits/support_create_ticket/support_create_ticket_cubit.dart\`
| \`test/screens/support/cubits/support_create_ticket_cubit_test.dart\`
| 9 |
|
\`settings_user_data/subpages/edit_name/cubit/settings_edit_name_cubit.dart\`
|
\`test/screens/settings_user_data/subpages/settings_edit_name_cubit_test.dart\`
| 8 |
|
\`settings_user_data/subpages/edit_phone_number/cubit/settings_edit_phone_number_cubit.dart\`
|
\`test/screens/settings_user_data/subpages/settings_edit_phone_number_cubit_test.dart\`
| 3 |
|
\`settings_user_data/subpages/edit_address/cubit/settings_edit_address_cubit.dart\`
|
\`test/screens/settings_user_data/subpages/settings_edit_address_cubit_test.dart\`
| 8 |
## What each file covers
- **support_chat_cubit:** Loaded with the mapped ticket; Error on
\`getTicket\` throw; \`sendMessage\` no-op when not in Loaded state and
for whitespace-only input; happy-path posts the message + re-fetches the
ticket (2 \`getTicket\` calls verified); service failure resets
\`isSending=false\` while keeping Loaded.
- **support_create_ticket_cubit:** initial state; \`selectType\` resets
reason to \`.other\`; \`selectReason\` preserves type; \`updateMessage\`
field-only; \`canSubmit\` gate matrix (type-null / reason-null / empty /
whitespace / submitting → all false); \`submit\` no-op when not
submittable; happy-path forwards type + reason + the mapped \`name\` for
both \`bugReport\` and \`genericIssue\` (pins the \`_getTicketName\`
switch); failure captures the error.
- **settings_edit_name_cubit:** Ready URL, Pending on \`inReview\`,
Failure on API throw, Failure on missing URL, \`refresh\` recovers;
\`submitName\` no-op when not Ready, posts the form payload + emits
Success, Failure on \`setData\` throw.
- **settings_edit_phone_number_cubit:** initial state; happy-path emits
\`[Submitting, Success]\` and forwards \`{'phone': N}\`; service throw
yields \`[Submitting, Failure]\` with the error message.
- **settings_edit_address_cubit:** same shape as edit_name plus two
address-specific pins — the nested payload (\`address.country.id\`) and
\`houseNumber\` is omitted from the payload when empty.
## Notes
- Cubits that fire work in their constructor (\`support_chat\`,
\`settings_edit_name\`, \`settings_edit_address\`) use
\`stream.firstWhere\` to await the final state instead of \`blocTest\`'s
sequence-based expect — same pattern as #329, with a one-line comment in
each test file header.
- \`SupportCreateTicketState.copyWith\` uses \`?? this.x\`, so
\`copyWith(selectedType: null)\` does **not** clear the field. The
\`canSubmit\` matrix builds the failing variants as fresh state objects
to avoid that footgun, and the rationale is on the test.
## Excluded (and why)
- \`settings_user_data_cubit.dart\` — touches three services + country
lookups + multi-path KYC step status; will be covered in a follow-up
alongside hook tests.
- \`settings_tax_report_cubit.dart\` — uses \`getTemporaryDirectory()\`
+ real \`File\` IO; needs path_provider platform-channel plumbing or a
refactor.
- \`dashboard\` blocs, \`create_wallet\` cubit, \`restore_wallet_cubit\`
— left for the next stage.
- \`dfx_kyc_service\` — covered indirectly via the edit cubits in this
PR; a dedicated test of its surface would overlap heavily with #319's
KYC cubit tests.
## Test plan
- [x] \`flutter analyze\` on all five new files — clean
- [x] \`flutter test\` — 34 / 34 passing locally
- [ ] CI green
…21 tests) (#331) ## Summary Stage 8 of the coverage push. Adds 21 unit tests across five previously-untested cubits. | Cubit under test | Test file | Cases | | --- | --- | --- | | \`create_wallet/bloc/create_wallet_cubit.dart\` | \`test/screens/create_wallet/create_wallet_cubit_test.dart\` | 4 | | \`restore_wallet/cubit/restore_wallet/restore_wallet_cubit.dart\` | \`test/screens/restore_wallet/restore_wallet_cubit_test.dart\` | 3 | | \`debug_auth/cubit/debug_auth_cubit.dart\` | \`test/screens/debug_auth/debug_auth_cubit_test.dart\` | 7 | | \`dashboard/bloc/balance_cubit.dart\` | \`test/screens/dashboard/balance_cubit_test.dart\` | 4 | | \`dashboard/bloc/pending_transactions_cubit.dart\` | \`test/screens/dashboard/pending_transactions_cubit_test.dart\` | 3 | ## What each file covers - **create_wallet_cubit:** initial hides the seed + has no wallet, \`createWallet\` stores the new \`SoftwareWallet\` (pinning the \`'Obi-Wallet-Kenobi'\` default name), \`toggleShowSeed\` flips and returns to hidden after two toggles, \`toggleShowSeed\` preserves the wallet field. - **restore_wallet_cubit:** initial state, \`restoreWallet\` canonicalises mixed whitespace (\` x y \` → \`x y\`) before delegating to \`WalletService.restoreWallet\`, the interim \`isLoading=true\` state is observable. - **debug_auth_cubit:** seeds \`address\` + \`savedSignature\` from the service; empty-address fallback when service has none; \`fetchSignMessage\` success + error; \`authenticate\` success + uses-state-address + error. - **balance_cubit:** initial zero-balance state shape \`(chain, contract, wallet, asset)\`; subscribes to \`BalanceRepository.watchBalance\` on init; emits each pushed balance through the stream; \`close()\` cancels the subscription cleanly. - **pending_transactions_cubit:** initial empty list; emits fetched list on construction; falls back to empty list (not state-error) when the service throws. ## Excluded (and why) - **dashboard_bloc** + transaction-history sub-cubits + portfolio/price chart cubits — pull in multiple services or larger event-driven flows; would dwarf this PR. - **transaction_history_receipt_cubit** / **settings_tax_report_cubit** — both use \`getTemporaryDirectory()\` + real \`File\` IO; needs path_provider platform-channel plumbing. - **sell_***, **sell_bitbox_***, **hardware_connect_bitbox_*** — Bitbox-coupled or DFX-sell-flow-coupled; held back to avoid review conflicts with #321 area. - **transaction_history_filter_cubit** — already covered by #327. ## Test plan - [x] \`flutter analyze\` on the five new files — clean - [x] \`flutter test\` — 21 / 21 passing locally - [ ] CI green
…oc (+22 tests) (#333) ## Summary Stage 9 of the coverage push. Adds 22 unit tests for the remaining dashboard cubits/bloc plus the global settings bloc. | Cubit / Bloc under test | Test file | Cases | | --- | --- | --- | | \`dashboard/bloc/price_chart/price_chart_cubit.dart\` | \`test/screens/dashboard/price_chart_cubit_test.dart\` | 4 | | \`dashboard/bloc/portfolio_chart/portfolio_chart_cubit.dart\` | \`test/screens/dashboard/portfolio_chart_cubit_test.dart\` | 5 | | \`dashboard/bloc/dashboard_transaction_history_cubit.dart\` | \`test/screens/dashboard/dashboard_transaction_history_cubit_test.dart\` | 4 | | \`dashboard/bloc/dashboard_bloc.dart\` | \`test/screens/dashboard/dashboard_bloc_test.dart\` | 3 | | \`settings/bloc/settings_bloc.dart\` | \`test/screens/settings/settings_bloc_test.dart\` | 6 | ## What each file covers - **price_chart_cubit:** empty-input zero-window state, all-period spots scaled by 100 + 10% Y-padding, \`selectPeriod\`-same is a no-op (no emit), \`oneWeek\` filter narrows to recent points. - **portfolio_chart_cubit:** empty-input zero-window state, all-period scaling + 6 horizontal-line values, flat-value series spreads via the 5% floor (no Y-collapse — pins the \`average * 0.05\` lower bound and the rounding to nice numbers \`{1,2,5,10}\`), \`selectPeriod\`-same no-op, \`oneWeek\` narrows visibleSpots. - **dashboard_transaction_history_cubit:** initial empty list, subscribes to \`watchTransactionsOfAssets\` with limit \`3\`, forwards every stream emission into state, \`close()\` cancels the subscription. - **dashboard_bloc:** initial state carries the supplied currency, the constructor \`refresh()\` populates price + priceChart + portfolioHistory via the services, \`CurrencyChangedEvent\` updates state and re-fetches all three datasets in the new currency. - **settings_bloc:** initial state reads through the repo, \`SetLanguageEvent\` writes \`'de'\` + emits, \`SetCurrencyEvent\` writes \`'EUR'\` + emits, \`SetNetworkModeEvent\` writes + calls \`getNewAuthToken\` + emits, \`ToggleHideAmountEvent\` flips both ways, a single toggle sets \`hideAmounts=true\`. ## Notes - The \`dashboard_bloc\` CurrencyChangedEvent test attaches a listener BEFORE adding the event because \`Bloc.stream\` is broadcast (no replay) and the event-driven re-fetch can complete before a follow-on \`firstWhere\` subscribes — same constraint we've now hit a few times in this push. ## Excluded (and why) - \`dashboard_bloc\` refresh after a service throw — non-trivial to test cleanly because the bloc lets the exception propagate out of the handler (which then surfaces as an unhandled bloc error in tests). Leaving as a follow-up. - \`transaction_history_receipt_cubit\`, \`transaction_history_multi_receipt_cubit\`, \`settings_tax_report_cubit\` — all use \`getTemporaryDirectory()\` + real \`File\` IO. - Buy / sell / sell_bitbox / hardware_connect_bitbox cubits — touched by the still-open PRs (#321 dashboard buy actions, #332 bitbox sign hardening); held back to avoid review conflicts. - \`settings_user_data_cubit\` — coordinates 3 services + country-lookup branches; deserves its own focused PR. ## Test plan - [x] \`flutter analyze\` on all five new files — clean - [x] \`flutter test\` — 22 / 22 passing locally - [ ] CI green
## Summary Stage 10 of the coverage push. Adds 21 unit tests for the two remaining easy-to-mock DFX backend services. | Service under test | Test file | Cases | | --- | --- | --- | | \`lib/packages/service/dfx/dfx_support_service.dart\` | \`test/packages/service/dfx/dfx_support_service_test.dart\` | 9 | | \`lib/packages/service/dfx/dfx_brokerbot_service.dart\` | \`test/packages/service/dfx/dfx_brokerbot_service_test.dart\` | 12 | ## What each file covers - **dfx_support_service:** \`getTickets\` GET shape (path, Bearer JWT) + ApiException on non-200; \`getTicket\` by uid + ApiException; \`createTicket\` POST body (type/reason/name + optional message) + omits message when null + requires status \`201\` (200 is rejected as ApiException); \`sendMessage\` POST shape + ApiException on non-201. \`getAuthToken\` is short-circuited by pre-populating \`sessionCache.authToken\` so the signing flow stays out of these unit tests. - **dfx_brokerbot_service:** \`getBuyPrice\` GET + currency-code query + invalid-input guards (non-numeric, zero, negative, non-200); \`getBuyShares\` GET + currency + invalid-input guards; \`getSellPrice\` with Bearer JWT + ApiException on non-200 + the invalid-input case skips the HTTP call entirely; same matrix for \`getSellShares\`. ## Notes - Same mocktail + \`http.testing.MockClient\` pattern as the previous DFX-service PRs (#326 / #328 / #329). - The "invalid input never reaches HTTP" assertions are a small but meaningful contract: callers can rely on these methods to fail fast before any network round-trip. ## Excluded (deferred) - \`settings_user_data_cubit\` was on the original Stage-10 plan but coordinates 3 services + Country lookups + multi-branch KYC-step-status handling. It deserves its own focused PR rather than tagging it onto these two service tests. - \`dfx_kyc_service\` — held back to avoid review conflicts with open PR #332 (KYC routing / bitbox sign hardening). - Buy / sell / Bitbox cubits — held back while #321 and #332 are open. ## Test plan - [x] \`flutter analyze\` on the two new files — clean - [x] \`flutter test\` — 21 / 21 passing locally - [ ] CI green
## Summary Stage 11 of the coverage push. | File under test | Test file | Cases | | --- | --- | --- | | \`lib/packages/service/dfx/real_unit_wallet_service.dart\` | \`test/packages/service/dfx/real_unit_wallet_service_test.dart\` | 3 | | \`lib/packages/service/dfx/real_unit_pdf_service.dart\` | \`test/packages/service/dfx/real_unit_pdf_service_test.dart\` | 9 | | \`lib/screens/home/bloc/home_bloc.dart\` | \`test/screens/home/home_bloc_test.dart\` | 5 | ## What each file covers - **real_unit_wallet_service:** \`getWalletStatus\` GET shape with the Bearer JWT, parses both \`isRegistered\` branches when \`userData\` is null, throws ApiException on non-200. - **real_unit_pdf_service:** \`getBalanceReport\` POSTs the BalancePdfDto payload (\`address\`, currency code, **uppercased** language code); accepts 201; ApiException on non-2xx. \`getTransactionsReceipt\` (multi) posts \`txHashes\` + currency, defaults to CHF, ApiException on non-2xx. \`getTransactionReceipt\` (single) posts \`txHash\` + currency, defaults to CHF, ApiException on non-2xx. - **home_bloc:** initial \`CheckWalletExistsEvent\` for three branches: no wallet → \`onboardingCompleted\` forced false; wallet+terms accepted → \`onboardingCompleted=true\`; wallet without terms → \`onboardingCompleted=false\`. \`CompleteOnboardingEvent\` writes \`termsAccepted=true\` and emits. \`AcceptSoftwareTermsEvent\` writes \`softwareTermsAccepted=true\` and emits. ## Notes - The PDF DTOs serialize \`txId\` as \`txHash\` on the wire (single) and \`txIds\` as \`txHashes\` (multi). The tests pin these names explicitly to lock the wire contract. - \`Language\` codes are uppercased in the balance-pdf payload (\`Language.de\` → \`'DE'\`). Also pinned. ## Excluded (and why) - Other \`home_bloc\` handlers (\`LoadCurrentWallet\`, \`DeleteCurrentWallet\`, \`LoadWallet\`, \`SyncWalletServices\`, \`DebugAuthComplete\`) all touch \`BalanceService.startSync\` / \`TransactionHistoryService.apiBasedSync\` / \`DfxWidgetService.getAuthToken\` plus \`AppStore.wallet\` mutation. Each is meaningful but needs more dependency plumbing — deserves its own focused PR. - \`settings_user_data_cubit\` — still deferred (coordinates 3 services + Country lookups + multi-branch KYC step status); will land separately. ## Test plan - [x] \`flutter analyze\` on all three new files — clean - [x] \`flutter test\` — 17 / 17 passing locally - [ ] CI green
…ts) (#336) ## Summary Stage 12 of the coverage push. Covers the previously-deferred \`settings_user_data_cubit\` (3-service coordination + Country lookups) and the read-side of \`transaction_history_service\`. | File under test | Test file | Cases | | --- | --- | --- | | \`lib/screens/settings_user_data/cubit/settings_user_data_cubit.dart\` | \`test/screens/settings_user_data/settings_user_data_cubit_test.dart\` | 6 | | \`lib/packages/service/transaction_history_service.dart\` (\`fetchPendingTransactions\` only) | \`test/packages/service/transaction_history_service_test.dart\` | 6 | ## What each file covers - **settings_user_data_cubit:** the cubit fans out to \`RealUnitWalletService.getWalletStatus\` and \`DfxKycService.getKycStatus\` in parallel, then either falls back to \`getUser\` (when userData is missing) or runs two more \`getCountryBySymbol\` lookups. Tests pin: - Full Success when userData is present (with nationality + addressCountry country lookups resolved to distinct \`Country\` instances). - \`pendingSteps\` only contains the three change steps (name/address/phone) that are in \`inReview\` — other inReview steps (e.g. \`contactData\`) are ignored. - \`userData == null\` + \`getUser\` returns mail → \`Success(email)\`, no country lookups happen. - \`userData == null\` + \`getUser\` throws → \`Success()\` with both userData and email null. - Failure when \`getWalletStatus\` throws (the \`Future.wait\` propagation). - Failure when \`getCountryBySymbol\` throws on a userData with a country code. - **transaction_history_service.fetchPendingTransactions:** no auth token short-circuits to \`[]\` without any HTTP call (verified via a MockClient that records calls); GET shape with the Bearer JWT to \`/v1/transaction/detail\`; non-200 returns \`[]\` (does not throw — intentional UX); filters out \`Completed\`/\`Failed\`/\`Returned\` (\`isPending=false\` per the enum extension); filters out transactions whose \`sourceAccount\` / \`targetAccount\` don't match the current wallet; wallet-match is **case-insensitive** (covers the lower-casing on both sides). ## Notes - \`RealUnitUserDataDto.type\` must use \`'HUMAN'\` / \`'CORPORATION'\` (the jsonName values of \`RegistrationUserType\`), not a friendly label — \`fromName\` throws \`StateError\` otherwise. - Mocktail's \`stream.firstWhere\` pattern is reused for the cubit (constructor fires \`getUserData\` synchronously, so we can't use \`blocTest\`'s sequence model reliably here — same as #329, #330, #333). ## Excluded (and why) - \`transaction_history_service.apiBasedSync\` — writes through \`TransactionRepository.insertDfxTransaction\`/\`updateTransaction\` and depends on \`AppStore.primaryAddress\` + \`apiConfig.asset.chainId\`. Doable but adds repository mock plumbing for a method that's almost entirely orchestration; will be its own focused PR. - Buy / sell cubits — still held while #321 (dashboard buy actions) and #332 (bitbox sign hardening) are open. ## Test plan - [x] \`flutter analyze\` on the two new files — clean - [x] \`flutter test\` — 12 / 12 passing locally - [ ] CI green
## Summary Stage 13 of the coverage push. Covers four cubits in the sell and buy flows whose source files are NOT touched by the currently-open #321 (dashboard buy actions, sell_page) or #332 (bitbox sign). | Cubit under test | Test file | Cases | | --- | --- | --- | | \`sell/cubits/sell_selected_bank_account/sell_selected_bank_account_cubit.dart\` | \`test/screens/sell/cubits/sell_selected_bank_account_cubit_test.dart\` | 3 | | \`sell/cubits/sell_balance/sell_balance_cubit.dart\` | \`test/screens/sell/cubits/sell_balance_cubit_test.dart\` | 4 | | \`sell/cubits/sell_bank_accounts/sell_bank_accounts_cubit.dart\` | \`test/screens/sell/cubits/sell_bank_accounts_cubit_test.dart\` | 6 | | \`buy/cubits/buy_converter/buy_converter_cubit.dart\` | \`test/screens/buy/cubits/buy_converter_cubit_test.dart\` | 9 | ## What each file covers - **sell_selected_bank_account_cubit:** initial null, \`selectBankAccount\` emits the provided account, \`selectBankAccount(null)\` clears the selection. - **sell_balance_cubit:** initial zero-balance derived from \`AppStore.apiConfig.asset\` + \`primaryAddress\`; subscribes to \`BalanceRepository.watchBalance\` on init; emits each pushed balance; \`close()\` cancels the subscription. - **sell_bank_accounts_cubit:** Success with DTO → BankAccount mapping on init; LoadFailure on \`getBankAccounts\` throw; \`add()\` calls \`createBankAccount\` + re-fetches; \`AddFailure\` preserves prior accounts + carries error message; \`deactivate()\` calls \`updateBankAccount(isActive=false)\` + re-fetches; \`UpdateFailure\` preserves prior accounts. - **buy_converter_cubit:** initial empty state; \`onFiatChanged\` debounces (100ms) and writes the converted shares; multiple rapid keystrokes only fire the LAST service call (pins the debounce contract); service error keeps state stable; \`onSharesChanged\` matches the output's fractional digits to the input (\`'5'\` → 2 digits, \`'5.000'\` → 3 digits — pins \`_fractionDigits\` behaviour); \`onCurrencyChanged\` re-fetches in the new currency and emits both fields; currency still flips on service error; \`close()\` cancels pending debounce timers so no service call happens after close. ## Excluded (and why) - \`sell_payment_info\`, \`sell_confirm\`, \`sell_converter\` (the parallel cubit to buy_converter), \`buy_confirm\`, \`buy_payment_info\` — each pulls in \`real_unit_buy_payment_info_service\` / \`real_unit_sell_payment_info_service\` / signing flows that are likely on PR #321's path. Held back to avoid review conflicts. - \`sell_bitbox_cubit\`, \`hardware_connect_bitbox_cubit\` — both directly touch BitBox; held while #332 is open. ## Test plan - [x] \`flutter analyze\` on the four new files — clean - [x] \`flutter test\` — 22 / 22 passing locally - [ ] CI green
## Summary Stage 14 of the coverage push. Adds 14 unit tests for the two confirm/converter cubits where the source files are not on PR #321's or #332's path. | Cubit | Test file | Cases | | --- | --- | --- | | \`buy/cubits/buy_confirm/buy_confirm_cubit.dart\` | \`test/screens/buy/cubits/buy_confirm_cubit_test.dart\` | 5 | | \`sell/cubits/sell_converter/sell_converter_cubit.dart\` | \`test/screens/sell/cubits/sell_converter_cubit_test.dart\` | 9 | ## What each file covers - **buy_confirm_cubit:** initial \`BuyConfirmInitial\`; happy path emits \`BuyConfirmSuccess(reference)\`; \`ApiException(statusCode: 503)\` → \`BuyConfirmFailure(BuyConfirmError.aktionariat)\` (pins the Aktionariat-down branch); other \`ApiException\` → \`BuyConfirmFailure(BuyConfirmError.unknown)\`; generic exception → \`unknown\`. - **sell_converter_cubit:** initial empty + CHF; \`onFiatChanged\` debounces (100ms) and writes shares from \`getSellShares\`; respects an explicit \`currency\` argument; debounce keeps only the last value (pins the per-keystroke contract); state stable on service error; \`onSharesChanged\` writes \`estimatedAmount\` with matching fractional digits (\`'10.000'\` → 3 digits, \`'10'\` → 2 digits); \`onCurrencyChanged\` calls \`getBuyPrice\` — NOT \`getSellPrice\` — with the current \`sharesText\` (pins the intentional buy-side estimation on currency switch); currency still flips even when \`getBuyPrice\` throws; \`close()\` cancels pending debounce timers so no service call after close. ## Notes - The \`sell_converter\` "currency switch uses BUY price" pin documents a non-obvious behaviour in the production code — leaving it untested would let a future refactor silently switch to \`getSellPrice\` and lose the no-fee preview. ## Excluded (still deferred) - \`buy_payment_info_cubit\`, \`sell_payment_info_cubit\`, \`sell_confirm_cubit\` — all touch \`real_unit_buy_payment_info_service\` / \`real_unit_sell_payment_info_service\` more deeply; #321 modifies these services + their tests, so I'm holding them back to avoid review conflicts. - \`sell_bitbox_cubit\`, \`hardware_connect_bitbox_cubit\` — BitBox-coupled; held while #332 is open. ## Test plan - [x] \`flutter analyze\` on the two new files — clean - [x] \`flutter test\` — 14 / 14 passing locally - [ ] CI green
## Summary Stage 15 of the coverage push. Two more DFX services. | Service | Test file | Cases | | --- | --- | --- | | \`lib/packages/service/dfx/dfx_widget_service.dart\` | \`test/packages/service/dfx/dfx_widget_service_test.dart\` | 5 | | \`lib/packages/service/dfx/real_unit_account_service.dart\` | \`test/packages/service/dfx/real_unit_account_service_test.dart\` | 4 | ## What each file covers - **dfx_widget_service:** \`wallet\` getter returns \`appStore.wallet.currentAccount\`; \`walletAddress\` returns the EIP-55 hex of the primary address (pinned to the Hardhat #0 address); \`isAvailable\` flips with the auth-token presence on the session cache. - **real_unit_account_service:** \`getPortfolioHistory\` GETs \`/v1/realunit/account/<eip55-hex>\` (path uses the wallet's primary address); parses \`valueChf\`/\`valueEur\` scaled by 100; treats null value as 0; returns \`[]\` on non-200 (does NOT throw — intentional). ## Notes - \`real_unit_account_service\` uses \`appStore.wallet.currentAccount.primaryAddress.address.hexEip55\` to build the request path. The test plumbs a real \`SoftwareWallet\` (Hardhat #0 mnemonic) through the AppStore mock so the EIP-55 hex is real, not stubbed. ## Test plan - [x] \`flutter analyze\` on the two new files — clean - [x] \`flutter test\` — 9 / 9 passing locally - [ ] CI green
…#340) ## Summary Stage 16 of the coverage push. | File | Cases (new) | | --- | --- | | \`buy/cubits/buy_payment_info/buy_payment_info_cubit.dart\` | 10 (new file) | | \`packages/service/balance_service.dart\` | 4 (appended to existing test) | ## What each file covers - **buy_payment_info_cubit:** initial \`BuyPaymentInfoInitial\`; happy CHF path emits Success; amount below 100 CHF minimum → \`MinAmountNotMetFailure\` and the service is NOT called; EUR minimum is scaled by \`getChfToEurRate\` and ceil'd (e.g. 100 × 0.92 → 92); EUR amount above scaled minimum proceeds to the service; empty amount string is treated as 0; comma decimal separator normalised to dot (e.g. \`'300,75'\` → 301); \`KycLevelRequiredException\` → \`Failure(kycRequired, requiredLevel)\`; \`RegistrationRequiredException\` → \`Failure(registrationRequired)\`; generic exception → \`Failure(unknown)\`. - **balance_service (4 new cases on top of the existing 2):** skips saving when the response JSON has no \`balance\` field; catches a non-numeric balance string and skips saving (production code does \`BigInt.parse\` inside try/catch); \`getBalance\` delegates straight through to \`BalanceRepository.getBalance\`; \`cancelSync\` is a safe no-op when called before \`startSync\`. ## Notes - \`buy_payment_info_cubit\` reads \`DFXPriceService.getChfToEurRate\` only for the EUR branch, so the CHF tests don't need to stub it. - The cubit's source file is NOT touched by PR #321 — only the underlying service is — so this PR is conflict-free. ## Test plan - [x] \`flutter analyze\` on the two changed files — clean - [x] \`flutter test\` — 16 / 16 passing locally (12 in buy_payment_info_cubit + 6 in balance_service incl. the 2 pre-existing) - [ ] CI green
## Summary Two sell-side cubits whose source files are NOT in PR #321's diff (only the underlying services are). | Cubit | Test file | Cases | | --- | --- | --- | | \`sell/cubits/sell_confirm/sell_confirm_cubit.dart\` | \`test/screens/sell/cubits/sell_confirm_cubit_test.dart\` | 3 | | \`sell/cubits/sell_payment_info/sell_payment_info_cubit.dart\` | \`test/screens/sell/cubits/sell_payment_info_cubit_test.dart\` | 12 | ## What each covers - **sell_confirm_cubit:** initial \`SellConfirmInitial\`; happy path passes through Loading and ends in Success (service mock suspends 10ms so the broadcast-stream listener can observe Loading); service throw → Failure carrying the error string. - **sell_payment_info_cubit:** - \`getPaymentInfo\` happy path emits Success with \`isBitbox=false\` for a \`SoftwareWallet\`; \`isBitbox=true\` for a \`BitboxWallet\` stub. - Exception mapping: \`KycLevelRequiredException\` → \`Failure(kycRequired, requiredLevel)\`; \`RegistrationRequiredException\` → \`Failure(registrationRequired)\`; generic → \`Failure(unknown, message)\`. - \`validateMinAmount\` (the parallel to buy_payment_info_cubit's min check, but here it's a CHF-10 floor): below floor emits \`MinAmountNotMet\`; above leaves the state untouched; EUR scaled by \`getChfToEurRate\` (ceil); a prior \`MinAmountNotMet\` is cleared back to \`Initial\` when the amount rises; comma decimal normalised to dot; empty string treated as 0. ## Notes - Both cubits use \`SellPaymentInfo\` which has 13 required fields. Tests construct one real instance with stub addresses + a no-op Eip7702Data so the value can be passed around without mocktail fallback registration. - For the BitBox-wallet test, a tiny \`_BitboxStubWallet extends AWallet\` is used (only \`walletType\` is read; \`primaryAccount\` / \`currentAccount\` throw to make the intent explicit). ## Test plan - [x] \`flutter analyze\` on both new files — clean - [x] \`flutter test\` — 15 / 15 passing locally - [ ] CI green
) ## Summary Stage 19 of the coverage push. Closes the previously-deferred apiBasedSync gap on TransactionHistoryService — kept in a separate file from \`fetchPendingTransactions\` (#336) to keep the repository mocks focused on the sync flow. | Method under test | Test file | Cases | | --- | --- | --- | | \`TransactionHistoryService.apiBasedSync\` | \`test/packages/service/transaction_history_service_sync_test.dart\` | 8 | ## What it covers - Non-200 on \`/v1/realunit/account/<addr>/history\` short-circuits: no repository writes (the cubit's early return when AccountHistoryDto is null). - Entries whose \`transfer\` field is null are skipped. - Inserts a plain \`Transaction\` when there is no matching DFX row. - Inserts a DFX-enriched transaction (\`insertDfxTransaction\`) when \`/v1/transaction\` has a row whose \`id\` is not null and whose \`inputTxId\` matches the history hash. - Existing transactions go through \`updateTransaction\` / \`updateDfxTransaction\` instead of insert (governed by \`existsTransaction\`). - The DFX-row match also accepts \`outputTxId\` — both directions are pinned. - DFX rows whose \`id\` is null fall back to the plain-Transaction insert path. ## Notes - Both endpoints are fetched via \`Future.wait\`. Reading \`request.url.path\` lets a single MockClient handler return different bodies for the two paths. - The test stubs \`existsTransaction\` to return false by default, then flips it to true for the update-path tests. ## Test plan - [x] \`flutter analyze\` on the new file — clean - [x] \`flutter test\` — 8 / 8 passing locally - [ ] CI green
## Summary - always show dashboard buy/sell actions and the empty-state Buy RealUnit button - make Buy/Sell/BankAccount/Sell brokerbot requests auth-aware by using lazy DFX auth and a one-time 401 refresh retry - remove the now-dead HomeState.isFiatServiceAvailable setup path ## Why The dashboard must not hide fiat entry points behind a transient in-memory auth token. After making the entry points visible, the downstream services also need to avoid sending Authorization: Bearer null or stale tokens. Authenticated requests now call getAuthToken() at first use and retry once with refreshAuthToken() on 401. ## Tests - flutter analyze - flutter test test/packages/service/dfx/dfx_auth_service_test.dart test/packages/service/dfx/real_unit_buy_payment_info_service_test.dart test/screens/buy/buy_page_test.dart test/screens/sell/sell_page_test.dart --------- Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
## Summary Stage 21 of the coverage push. Closes the \`dfx_kyc_service\` surface added in #343. | Method | Cases | | --- | --- | | \`continueKyc\` | 2 | | \`startStep\` | 2 | | \`setData\` | 2 | | \`getFinancialData\` | 3 | | \`setFinancialData\` | 2 | ## What each method pins - **continueKyc:** PUT \`/v2/kyc\` with the \`x-kyc-code\` header from \`getUser().kyc.hash\`; parses \`KycSessionDto\`; \`ApiException\` on non-2xx. - **startStep:** GET \`/v2/kyc/<stepName.value>\` (the path encodes the step enum's wire string); parses \`KycSessionDto\`; \`ApiException\` on non-2xx. - **setData:** PUTs the caller-provided session URL (NOT the host) with the JSON body + \`x-kyc-code\` header; \`ApiException\` on non-2xx. - **getFinancialData:** GET \`<url>?lang=<lang.code>\`; defaults the language to \`Language.de\` (\`'de'\`); \`x-kyc-code\` header; \`ApiException\` on non-2xx. - **setFinancialData:** PUTs \`{ "responses": [...] }\` to the caller-provided URL with \`x-kyc-code\` header; serialises each \`KycFinancialResponse\` to \`{ key, value }\`; \`ApiException\` on non-2xx. ## Conflict avoidance Same as #343 — \`dfx_kyc_service.dart\` is NOT in PR #332's diff. The service surface is stable on that branch. ## Test plan - [x] \`flutter analyze\` clean - [x] \`flutter test\` — 11 / 11 passing locally - [ ] CI green
## Summary
Stage 20 of the coverage push. Covers four core methods of the
previously-deferred \`dfx_kyc_service\`.
| Method | Cases |
| --- | --- |
| \`getUser\` | 3 |
| \`updateUser\` | 3 |
| \`request2FaCode\` | 2 |
| \`verify2FaCode\` | 2 |
## What each method's tests pin
- **getUser:** GET \`/v2/user\` with Bearer JWT + parses \`UserDto\`;
201 also accepted (in addition to 200); throws \`ApiException\` on a
non-2xx non-401 (the 401-refresh-and-retry path is large enough to
deserve its own test, deferred).
- **updateUser:** PUT \`/v2/user\` with the JSON body; 201 accepted;
\`ApiException\` on non-2xx.
- **request2FaCode:** POST \`/v2/kyc/2fa?level=Strict\` with
\`x-kyc-code\` header set from \`getUser().kyc.hash\` (pins the kyc-hash
propagation); \`ApiException\` on non-2xx.
- **verify2FaCode:** POST \`/v2/kyc/2fa/verify\` with \`{ token: code
}\` body and the same \`x-kyc-code\` header; \`ApiException\` on
non-2xx.
## Conflict avoidance
PR #332 (bitbox sign + KYC routing) touches
\`lib/screens/kyc/cubits/kyc/kyc_cubit.dart\` and
\`real_unit_registration_service.dart\` — NOT this service.
\`dfx_kyc_service.dart\` itself is unchanged on that branch, so this PR
is conflict-free.
## Notes
- Tests use a tiny \`_StubWallet\` so \`DFXAuthService.wallet\` resolves
without plumbing a real \`SoftwareWallet\` through.
- The wire format for \`kyc.level\` is the numeric value (e.g. \`20\` →
\`KycLevel.level20\`), pinned in the test fixture so a future enum / DTO
refactor surfaces immediately.
## Excluded (deferred)
- \`getUser\`'s 401 → \`refreshAuthToken\` → retry path needs a stateful
MockClient + the auth-service signing flow; deserves its own focused
test.
- \`continueKyc\`, \`startStep\`, \`setData\`, \`getFinancialData\`,
\`setFinancialData\` — more elaborate bodies + headers; will follow up
if/when needed.
## Test plan
- [x] \`flutter analyze\` clean
- [x] \`flutter test\` — 10 / 10 passing locally
- [ ] CI green
Closes Acceptance Criterion #6 of #314 — _\"Documentation: \`docs/testing.md\` explains when to use which tier, with examples.\"_ ## Summary A single page that captures everything we've learned across Phase 0 (#319) and Phase 1 Foundation (#320 / #321) about how to test BitBox-touching code without re-deriving the rules every PR: - **Tier matrix** (0–4) with hardware / CI status for each, so reviewers can tell at a glance what a test does and doesn't prove - **Decision tree** for picking a tier — \"if a Tier 0 test would have to mock the very thing under test, drop down a tier\" - **Tier 0 patterns** with concrete excerpts from real test files: - Cubit / Bloc tests (\`bloc_test\` + \`mocktail.Mock\`) pointing at \`test/screens/kyc/cubits/kyc/kyc_cubit_test.dart\` - Widget tests (\`pumpApp\` + \`MockCubit\`) pointing at \`test/screens/kyc/steps/kyc_email_page_test.dart\` - Service + HTTP tests (\`http/testing\` \`MockClient\` + \`_MockAppStore\`) pointing at \`test/packages/service/dfx/dfx_bank_account_service_test.dart\`, including the **\`setAuthToken('test-jwt')\` pre-seed** trick for DFXAuthService-derived services (the gotcha that broke CI on #321) - **Tier 1 patterns** for \`FakeBitboxCredentials\` with the \`FakeBitboxBehavior\` matrix and the disconnect-flip-to-success reconnect pattern, pointing at \`test/integration/kyc_sign_flow_test.dart\` - **Tiers 2–4** marked deferred with status pointers back to the corresponding phase of #314 - **Mocktail gotchas** (\`Future\` + \`thenAnswer\`, \`registerFallbackValue\`, private mocks) - **Add-tests checklist** for PRs touching \`KycCubit\` / \`Eip712Signer\` / \`DFXAuthService\` / \`BitboxCredentials\` / \`bitbox_flutter\` CONTRIBUTING.md's Testing section gets a one-line cross-link. ## Test plan - [x] Doc compiles / renders (no broken intra-repo links in the markdown) - [x] Every code example matches a real file already on \`develop\` - [x] \`flutter analyze\` — no new issues (4 pre-existing errors in \`test/screens/home/home_bloc_test.dart\` are unrelated to this PR; verified they exist on \`develop\` too)
…346) ## Summary Stage 23 of the coverage push. Three small pure-Dart targets that round out the config + widget-extension surface. | File | Cases (new) | | --- | --- | | \`lib/packages/config/network_mode.dart\` | 3 (new file) | | \`lib/packages/config/api_config.dart\` | 6 (appended to existing) | | \`lib/widgets/mnemonic_field.dart\` (\`SeedStringExtension\`) | 5 (new file) | ## What each file covers - **network_mode:** mainnet / testnet getters + name; \`NetworkMode.values\` has exactly 2 entries (a third would silently bypass every \`switch (mode)\` call site). - **api_config (extension):** testnet \`asset\` = \`realUnitTestAsset\`, testnet ids = Sepolia; mainnet \`asset\` = \`realUnitAsset\`, mainnet ids = Ethereum; \`buildUri\` produces https URIs; appends queryParams when provided; omits the query string when null. - **mnemonic_field SeedStringExtension:** splits a 12-word mnemonic correctly; collapses tabs and multi-space runs; empty / whitespace-only input → \`[]\`; trims leading/trailing whitespace; preserves word order. ## Test plan - [x] \`flutter analyze\` clean - [x] \`flutter test\` — 17 / 17 passing locally (incl. the 2 pre-existing api_config cases) - [ ] CI green
) ## Summary Stage 22 of the coverage push. Pure-Dart utility files. | File | Cases | | --- | --- | | \`lib/packages/utils/svg_parser.dart\` | 5 | | \`lib/packages/utils/device_info.dart\` | 5 | | \`lib/packages/utils/default_assets.dart\` | 4 | ## What each file covers - **svg_parser:** integer + decimal mm → px conversion at 96 DPI (≈ 3.7795 px/mm), non-mm units pass through unchanged, every \`mm\` occurrence is replaced (multiple per string), surrounding SVG markup is preserved. - **device_info:** iOS / Android / macOS / Windows+Linux / Fuchsia matrix using \`debugDefaultTargetPlatformOverride\` (save + restore around each test so a failure doesn't leak the override). - **default_assets:** mainnet \`realUnitAsset\` and Sepolia \`realUnitTestAsset\` pinned to their production / dev contract addresses + decimals + symbols (a typo here would silently route the entire fiat pipeline at the wrong contract). ETH and ZCHF asset-id constants also pinned. ## Test plan - [x] \`flutter analyze\` clean - [x] \`flutter test\` — 14 / 14 passing locally - [ ] CI green
## Summary Stage 24 of the coverage push. The \`SettingsRepository\` wraps SharedPreferences and is one of the few storage-adjacent files that can be tested directly via \`SharedPreferences.setMockInitialValues\`. | Group | Cases | | --- | --- | | currentWalletId | 3 | | language | 3 | | currency | 3 | | terms (terms + softwareTerms) | 3 | | networkMode | 4 | ## What each group covers - **currentWalletId:** null when not stored; \`saveCurrentWalletId\` persists; \`removeCurrentWalletId\` clears. - **language:** falls back to \`'en'\` for non-German system locales (PlatformDispatcher locale in tests is en_US); returns stored value when set; setter persists. - **currency:** defaults to \`'CHF'\` when not stored; returns stored value; setter persists. - **terms:** both \`termsAccepted\` and \`softwareTermsAccepted\` default to false; setters persist; setting one does not flip the other (independent flags). - **networkMode:** defaults to mainnet when no value AND when stored value is unknown (\`firstWhere\`'s \`orElse\`); reads via the **enum constructor argument name** (\`'Mainnet'\` / \`'Testnet'\`) — not the Dart enum identifier (this trap surfaced during the tests because the production code uses \`.name\` and the constructor arg shadows it); setter persists. ## Test plan - [x] \`flutter analyze\` clean - [x] \`flutter test\` — 16 / 16 passing locally - [ ] CI green
## Summary Stage 25 of the coverage push. Three small pure-Dart targets. | File | Cases | | --- | --- | | \`lib/packages/utils/asset_logo.dart\` | 7 | | \`lib/models/blockchain.dart\` | 8 | | \`lib/packages/utils/xfile_extension.dart\` | 7 | ## What each file pins - **asset_logo:** mainnet ETH (\`chainId:1 + 0x0\`) → \`ETH.png\`; mainnet RealUnit + Sepolia RealUnit → \`REALU.png\`; mixed-case address still resolves REALU (production lowercases first); unknown chain/address falls back to \`REALU.png\` (pinned — changing this would silently render every unknown asset as ETH instead). \`getChainImagePath\` for mainnet + Sepolia. - **Blockchain:** \`Blockchain.values\` has exactly 2 entries; chainId / name / nativeSymbol for both; \`getFromChainId\` resolves the known ids and throws \`StateError\` on unknown; \`nativeAsset\` shape (address \`0x0\`, decimals 18). - **xfile_extension:** \`toBase64DataUri\` uses an explicit \`mimeType\` when present; falls back to extension-based guessing for \`.png\` / \`.jpg\` / \`.JPEG\` (case-insensitive) / \`.pdf\`; defaults to \`application/octet-stream\` for unknown extensions; the base64 segment encodes the raw file bytes (verified by round-tripping a UTF-8 payload). ## Notes - \`xfile_extension\` tests write a real \`XFile\` against a per-test temp directory so the production code's \`readAsBytes\` path runs unchanged. No platform-channel plumbing needed. ## Test plan - [x] \`flutter analyze\` clean - [x] \`flutter test\` — 22 / 22 passing locally - [ ] CI green
## Summary Stage 26 of the coverage push. Pure-Dart model classes. | Model | Cases | | --- | --- | | \`Asset\` | 5 | | \`Balance\` | 5 | | \`Transaction\` | 3 | | \`TransactionTypes\` enum | 1 | ## What each model pins - **Asset:** \`id\` derived from \`fastHash(chainId:address)\`; \`Asset.getId\` static matches the instance getter; different chains / addresses produce different ids; \`id\` depends only on chain+address, not on metadata (\`name\` / \`symbol\` / \`decimals\` don't affect identity — important for repo de-duplication). - **Balance:** \`id\` from \`fastHash(wallet:chain:contract)\`; \`balance\` amount is mutable; two Balances with the same identity tuple are \`==\` regardless of amount (pinned so a stream-emitted update with a new amount can still match an existing map key); different chain / wallet differs. - **Transaction:** \`isOutbound\` true when sender matches wallet (EIP-55 normalised); false when sender differs; normalises hex-digit case via \`fromHex\` / \`hexEip55\` on both sides (NOT case-insensitive on the \`0x\` prefix — that breaks fromHex; the test pins the actual contract). - **TransactionTypes:** values has exactly 5 entries (catches an accidental addition that would silently break the rendering switch). ## Test plan - [x] \`flutter analyze\` clean - [x] \`flutter test\` — 14 / 14 passing locally - [ ] CI green
## Summary Stage 27 of the coverage push. Wire-format DTOs that were missing direct tests — important to pin because they encode the contract with the DFX backend. | DTO | Cases | | --- | --- | | \`PriceStep.fromJson\` | 2 | | \`BroadcastTransactionRequestDto.toJson\` | 1 | | \`BroadcastTransactionResponseDto.fromJson\` | 1 | | \`RealUnitUnsignedTransactionsRequestDto.fromJson\` | 1 | | \`RealUnitSellConfirmDto.toJson\` | 4 | | \`Eip7702DelegationDto.toJson\` | 1 | ## What's pinned - **PriceStep:** full wire shape (\`source\` / \`from\` / \`to\` / \`price\` / \`timestamp\`); integer \`price\` widens to double (matches the \`num\` cast in production). - **Broadcast req/resp:** request serialises the four signature parts (\`unsignedTx\`, \`r\`, \`s\`, \`v\`); response extracts \`txHash\`. - **Unsigned-transactions request:** parses \`swap\` + \`deposit\`. - **SellConfirmDto:** the conditional-key serialisation (4 branches — txHash-only, eip7702-only, both, neither/empty). This is load-bearing on the wire and a regression here would silently send empty confirms. - **Eip7702DelegationDto:** round-trips all five fields. ## Test plan - [x] \`flutter analyze\` clean - [x] \`flutter test\` — 10 / 10 passing locally - [ ] CI green
Collection PR for follow-ups identified during the #541 review session. Held as a **Draft** while commits accumulate; flipped to ready + merged at the end. ### Likely contents (subject to scope decisions) - `lib/styles/text_styles.dart` — hardcode `fontFamily` directly in `_Body.base/.sm/.xs` + `_Header.h1/.h2/.h4` (root-cause for the latent `appBarTheme.titleTextStyle` Ahem-rendering bug; supersedes the per-theme `copyWith(fontFamily: …)` pin added in #562) - `test/integration/wallet_creation_bitbox_test.dart:194-195` — drop the tautological `appStore.wallet = created; verify(...).called(1);` or replace with a real contract pin - `test/integration/connect_bitbox_flow_test.dart:249,285` — align wall-clock `Future.delayed(fastObserverInterval * 4)` with the `fakeAsync`+`async.elapse` pattern used in `bitbox_reconnect_recovery_test.dart` - `test/goldens/screens/legal/legal_document_golden_test.dart` — replace inline `_termsMarkdownStub` (1:1 copy of production terms) with a synthetic markdown fixture under `test/fixtures/` - Investigate why the dfx01 runner produces ±10–50 byte encoder drift on unrelated golden PNGs during baseline refresh (Skia version pin, build cache, font cache) ### Out of scope - Anything that should go to a dedicated PR (e.g. a real new feature) - #325 release develop → main ### Notes Empty seed commit kept in history so the PR existed before the first real change landed. --------- Co-authored-by: Blume1977 <jana.ruettimann@dfx.swiss> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Empty seed commit so a draft PR can exist before the actual follow-ups land. Real changes (further goldens state-coverage, BitBox-aware variant pinning, additional handbook slots, etc.) get pushed on top of this branch and are visible incrementally in the PR diff.
…586) ## Summary Both `TransactionIssue` and `LimitRequest` options in the in-app support ticket type picker silently fail with HTTP 400 on submit because the app never sends the API-required nested DTOs (`transaction` / `limitRequest`). The reproduction in the field is `RealUnitApiException: limitRequest should not be empty (statusCode: 400)` for LimitRequest; the TransactionIssue equivalent has not surfaced because realunit-app users typically file transaction issues via services.dfx.swiss instead. This PR hides both options from the picker until the proper forms are implemented. The enum values, i18n keys (`supportTransactionIssue`, `supportLimitRequest`), icons, and the cubit's `_getTicketName` mapping are intentionally left in place so re-enabling is a one-tuple revert per type once the structured input UI (dropdowns, transaction picker, document upload) lands. ## Why not implement the forms now - `LimitRequest` needs three dropdowns (`limit` / `investmentDate` / `fundOrigin`), an optional `fundOriginText` field mapped from message, a KYC ≥ 50 gate, and a mandatory document upload (PNG / JPEG / PDF). Requires a new `file_picker` dependency for PDF support. - `TransactionIssue` needs a transaction picker (list of the user's recent transactions via `GET /v1/transactions`) plus a reason dropdown filtered to `[FundsNotReceived, TransactionMissing, Other]`. - Both are independent feature PRs of meaningful scope; hiding now removes the user-facing failure immediately. ## Scope - `lib/screens/support/subpages/support_create_ticket_page.dart` — remove two tuples from the `items:` array of `TagSelection<SupportIssueType>`. - No API, model, cubit, service, or i18n changes. ## Test plan - [x] `flutter analyze` → `No issues found! (ran in 4.7s)` - [x] `flutter test test/screens/support/` → 62 / 62 passed - [ ] Manual smoke: open `Neues Ticket erstellen`, confirm only `Allgemeines Anliegen`, `Fehlerbericht`, `KYC-Problem` are selectable. - [ ] Manual smoke: submit each of the three remaining types with a short message and verify a ticket is created (no 400).
Re-opens [#584](#584) (closed because its base \`chore/post-541-followups\` was deleted after the collection-merge [#571](#571)). Cherry-picked Jana's original commit (\`517170a\`) onto the new collection branch \`chore/post-580-followups\`. ## Summary The "Support kontaktieren" tile under Settings → Kontakt must always be visible — including pre-signin onboarding flows where the user has not yet registered an email. Render it unconditionally and drop the surrounding cubit/state machinery that only existed to gate this single tile. ## Pair PR Pairs with [DFXswiss/api#3761](DFXswiss/api#3761) (drops \`UserCapabilitiesDto.supportAvailable\`). **Merge order is unconstrained — both PRs are independent-safe:** | Order | Backend sends | App reads | Crash? | |---|---|---|---| | App-PR alone merged | \`supportAvailable: true/false\` still in JSON | Field no longer read in \`fromJson\` — Dart \`Map<String,dynamic>\` silently ignores unknown keys | No | | API-PR alone merged | Field gone from JSON | Old app code reads \`json['supportAvailable'] as bool? ?? false\` → \`false\` → tile stays hidden (existing bug persists) | No | | Both merged | Backend stops sending, app stops reading | — | No | The DTO uses \`as bool? ?? false\` (nullable cast with default fallback) for every capability flag, so neither side is brittle to the other's deploy timing. Earlier "Merge order: API first" claim was inaccurate. ## Review history Audited via subagent during the #584 cycle (clean, no MAJORs). Main-repo mirror was needed for Visual Regression on the dfx01 self-hosted runner (same Fork-PR pattern as [#585](#585)). Credit: code-diff by Jana Rüttimann (\`Blume1977\`). --------- Co-authored-by: Blume1977 <jana.ruettimann@dfx.swiss> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…set (#589) ## Summary Mirror the API-side `@IsSwissPaymentText()` validator (DFXswiss/api#3766) in the four user flows that write into `user_data`: - KYC registration — personal step (`firstName`, `lastName`) - KYC registration — address step (`street`, `houseNumber`, `postalCode`, `city`) - Settings → edit name (`firstName`, `lastName`) - Settings → edit address (`street`, `houseNumber`, `postalCode`, `city`) Users now get immediate field-level feedback in the form if they enter a character outside the SIX SIG IG QR-Bill v2.3 permitted character set, instead of submitting and waiting for a generic 400 from the backend. ## Why DFX business decision: only characters valid in Swiss payment systems are supported end-to-end. Pairs with the umlaut-preservation fix in DFXswiss/api#3766 — now that diacritics survive storage and PDF rendering, we also lock the input surface so CJK / Cyrillic / Arabic / emoji never enter the system. ## Character set ``` [\x20-\x7E] (printable ASCII) + À Á Â Ä Ç È É Ê Ë Ì Í Î Ï Ñ Ò Ó Ô Ö Ù Ú Û Ü Ý ß + à á â ä ç è é ê ë ì í î ï ñ ò ó ô ö ù ú û ü ý + \n (line feed) ``` Covers German, French, Italian, Romansh. Regex is byte-for-byte aligned with the API regex (`Config.formats.swissPaymentText`) so any input the client accepts the backend also accepts. ## Changes - `lib/packages/utils/swiss_payment_text.dart`: new `isSwissPaymentText(String?)` helper (empty/null = valid). - Four screen files: add the helper call to existing `validator:` callbacks; missing validator on house-number in settings edit-address page added. - `assets/languages/strings_en.arb`, `strings_de.arb`: new `swissPaymentTextInvalid` translation key, inserted in alphabetical order. - Generated `lib/generated/i18n.dart` is gitignored — built by `dart run tool/generate_localization.dart` after pulling. ## Test plan - [ ] `dart run tool/generate_localization.dart` runs without errors after checkout - [ ] `flutter analyze` — passes (verified locally, no new issues) - [ ] Type `Rüttimann` / `Münchwilen` / `Genève` / `Saint-Légier` in any of the four flows → no error, form accepts - [ ] Type `王小明` / `Иван` / `日本語` / an emoji → field shows the Swiss-payment-text error message - [ ] Submit a valid form → API call succeeds (i.e. client and server regex match) - [ ] German UI shows German error message; English UI shows English message - [ ] `flutter test` — pre-existing golden failures unchanged (verified locally — same 88 failures on clean develop)
Add the missing test coverage for `lib/packages/utils/swiss_payment_text.dart` (introduced in #589) so the regex character set is locked in against regressions when the API-side `Config.formats.swissPaymentText` drifts. 33 cases across 8 groups: - empty / null → valid (matches the docstring contract: callers chain a non-empty check) - printable ASCII (0x20–0x7E): digits, punctuation, full 95-char band - uppercase + lowercase Latin diacritics for CH / DE / FR / IT - real Swiss names + addresses (Rüttimann, Münchwilen, Genève, Saint-Légier, D'Hauterive, François) - non-Latin scripts rejected (CJK, Cyrillic, Arabic, Hebrew, emoji) - mixed-script attacks (Latin word with a Cyrillic homoglyph е inside) - whitespace: newline accepted (multi-line memos), tab + CR rejected - uncommon Latin diacritics rejected (Polish ąć, Portuguese ã, Norwegian øå, French ligature œ, French Ÿ) — explicitly documents what is NOT in the Swiss payment set so the regex is hand-verifiable Run: `flutter test --no-pub test/packages/utils/swiss_payment_text_test.dart` → 33/33 pass.
## Summary - Handbook (`docs/handbook/de/index.html:1088-1090`) behauptet, die Wallet-Sicherung-Kachel ist bei BitBox-Wallet ausgeblendet - Bestehender Golden deckt nur die Software-Wallet-Variante ab — Invertierung der Bedingung in `settings_page.dart:100` würde nicht auffallen - Neuer `goldenTest` „bitbox wallet open hides the Wallet-Sicherung tile" sichert die hidden-tile-Logik ab - `MockBitboxWallet` in `test/helper/golden_mocks.dart` ergänzt (analog zu `MockSoftwareWallet`) Schließt Reviewer-Finding #2 aus dem #582-Review. ## Test plan - [ ] CI grün (Visual Regression, Analyze & Test, Coverage Floor Gate, Handbook Build Check) - [ ] Goldens via `golden-regenerate.yaml` auf dfx01 generiert (erwartet: 1 neuer PNG `settings_page_bitbox.png`) - [ ] Manuell verifiziert: PNG zeigt Settings-Page ohne Wallet-Sicherung-Tile --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…592) Follow-up to the BitBox-Goldens deep-dive done after [#580](#580). Of the 8 candidate BitBox connection sub-states, 3 are pixel-distinct UI patterns worth pinning; the other 5 are either identical to the baseline, near-variants of the top-3, or have no UI at all. ## Added (top-3 by ROI) | File | State | Why pinned | |---|---|---| | `connect_bitbox_page_connecting.png` | `BitboxConnecting` | Only spinner-only state with neither confirm nor cancel — distinct \`ConnectContent\` branch | | `connect_bitbox_page_check_hash.png` | `BitboxCheckHash` | Highest pixel-drift risk: alpha-blended pairing-code pill with custom BoxDecoration | | `connect_bitbox_page_signature_failed.png` | `BitboxSignatureFailed` | Only state with two override-labelled buttons (Retry + Continue anyway) | ## Out of scope (deliberately) | State | Reason | |---|---| | `BitboxFound` | Renders identical to \`BitboxNotConnected\` — view's switch has no case, falls to \`_ =>\` default | | `BitboxPairing` | Spinner subtitle variant of \`BitboxConnecting\` — near-duplicate | | `BitboxCapturingSignature` | Title-only variant of \`BitboxPairing\` | | `BitboxConnected` | Button + title variant of the connected SVG branch — close to \`SignatureFailed\` | | `BitboxFinishSetup` | Emits no UI, only a BlocListener \`onFinish(wallet)\` callback | ## Coverage gap closed The view file \`lib/screens/hardware_connect_bitbox/connect_bitbox_view.dart\` has three orthogonal branches in its state-switch (spinner-only, hash-pill, two-button-with-override-labels). Before this PR, only the default \"no UI ceremony\" branch (\`BitboxNotConnected\`) was pixel-locked. Cubit-tests (16 in \`connect_bitbox_cubit_test.dart\`) cover the state-machine logic, view-tests cover button wiring — but pixel-layout regression of these three branches was structurally unguarded. ## Mock setup Lifted verbatim from \`test/screens/hardware_connect_bitbox/connect_bitbox_view_test.dart\`: \`_FakeBitboxDevice extends Fake\`, \`_MockBitboxWallet extends Mock\`. No new helper file, no new pubspec entries. ## Pending CI dance - Bot golden-regen workflow will commit the three new baselines on the dfx01 self-hosted runner. - Empty re-trigger commit needed afterwards per the GITHUB_TOKEN gotcha documented in \`docs/visual-regression-tests.md\`. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Rolling collection branch for follow-ups after [#580](#580) (goldens max coverage + handbook 52 slots) merged into develop via [#571](#571). Same pattern as the previous `chore/post-541-followups` collection. ## Mechanics - Feature PRs target this branch as their base. Each lands as a separate PR for review history. - This collection PR aggregates them; when stable, it's merged into develop in one go. - Initial commit is intentionally empty — actual changes land on top. ## Known follow-up topics Tracked from the #580 audit + day-2 fixes: - BitBox-Wallet-Variante des Settings-Goldens (`settings_page_bitbox`) — die HTML-Erklärung im Handbook erwähnt das Hiding der Wallet-Sicherung-Tile bei BitBox, kein Golden pinnt es derzeit. - BitBox-Connection-State-Goldens — aktuell nur `BitboxNotConnected` ist als Golden. Weitere 8 States (Found, Connecting, CheckHash, Pairing, CapturingSignature, SignatureFailed, Connected, FinishSetup) sind theoretisch renderbar via Mock-Wallet. - Eventuelle Locale-`en`-Goldens für die Pages, wo lange deutsche Strings Layout-relevant sind (Buy/Sell Banner). --------- Co-authored-by: Blume1977 <jana.ruettimann@dfx.swiss> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…red-wording feat(i18n): broaden purchase pre-flight message wording
…#597) ## Symptom (v1.0.67, reproduced live) Open Buy page → quickly delete the prefilled \`300\` → type \`4600\` → **Result-Feld zeigt 321** statt der korrekten 3216 REALU. Erneutes Löschen + Tippen von \`4600\` zeigt 3216. \`floor(460 / 1.43) = 321\` — der Smoking Gun: die App hat das Resultat einer API-Anfrage für \`amount=460\` angezeigt, obwohl der Controller \`4600\` enthält. ## Root cause \`_fiatDebounce?.cancel()\` storniert nur **noch nicht gefeuerte** Timer. Sobald ein Timer-Body läuft, schickt er seine API-Anfrage raus und emittet das Resultat bei Rückkehr — unabhängig davon, was der User inzwischen tippt. Konkrete Sequenz für das Symptom: \`\`\` T=0 '4' → emit fiatText:'4', Timer→100ms T=20 '6' → cancel, Timer→120ms T=40 '0' → cancel, Timer→140ms ← fiatText='460' jetzt T=140 Timer für '460' feuert: emit loading:true, API_460 startet ─┐ T=160 '0' → cancel (zu spät!), Timer→260ms │ in flight T=260 Timer für '4600' feuert: API_4600 startet ────┐ │ T=380 API_4600 antwortet → shares:3216 → emit ✓ │ │ T=420 API_460 antwortet später → shares:321 → emit ← ÜBERSCHREIBT \`\`\` Zwei in-flight Requests, last-write-wins, der ältere gewinnt nicht-deterministisch je nach Latenz. ## Fix Monotoner Counter \`_seq\` im Cubit. Jede User-Methode (\`onFiatChanged\`, \`onSharesChanged\`, \`onCurrencyChanged\`) inkrementiert ihn synchron und übergibt den Wert via Closure-Capture an den async-Body. Der Body prüft vor jedem \`emit\` (vor und nach dem \`await\`), ob \`_seq\` weitergewandert ist, und droppt sich stillschweigend falls ja. \`onCurrencyChanged\` flippt \`state.currency\` zusätzlich **vor** dem \`await\` — der Picker reagiert sofort, auch wenn die nachfolgende Konversion vom Seq-Guard verworfen wird. Existing Test \`'onCurrencyChanged still flips currency even on service error'\` bleibt grün. ## Tests **5 neue Race-Tests** (3 buy, 2 sell), die in-flight Responses out of order completen und pinnen dass der Seq-Guard die alten droppt — also direkt das Symptom aus v1.0.67 pinnen. **Bestehende Tests** (28 cubit, 180 buy/sell breit) bleiben grün ohne Anpassungen. ## Verifiziert lokal - \`flutter analyze lib/screens/buy/cubits/buy_converter/ lib/screens/sell/cubits/sell_converter/ test/screens/buy/cubits/ test/screens/sell/cubits/\` → clean - \`flutter test test/screens/buy/cubits/buy_converter_cubit_test.dart test/screens/sell/cubits/sell_converter_cubit_test.dart\` → 28/28 - \`flutter test test/screens/buy/ test/screens/sell/ test/screens/buy_sell_converter_confirm_states_test.dart\` → 180/180 ## Was bewusst NICHT in dieser PR ist - **D1** Stale \`sharesText\` bei API-Fehler (verschieden vom Race; separater Pfad) - **D2** Komma-Sanitization im Brokerbot-Service (PaymentInfo macht es schon, Converter nicht — separate PR) - **D3** Loading-Indikator + Error-Surface in der UI (Welle 2) - **Eager Controller-Init / focus-aware Sync** (UX-PR, ändert Behavior) Diese sind reale Defekte aber **nicht Ursache** des reproduzierten Symptoms. Sie verschmutzen den Race-Fix nur und werden separat adressiert.
## Summary
The support-chat bubble shipped with a broken sender check —
`isFromSupport => author == null` — that never matched, because the
backend always sends a non-null `author` string:
| Sender | `author` value (from `support-message.entity.ts`) |
|---|---|
| Customer | `'Customer'` (the `CustomerAuthor` constant) |
| Bot | `'AutoResponder'` |
| Support agent | clerk name from the `supportClerks` setting (fallback
`'Support'`) |
As a result every message — including the auto-responder's "Hi Jana, …"
template — was rendered as a user message: right-aligned, in
`realUnitBlue`. The screenshot below the PR description shows the visual
collision.
## Changes
- `support_message.dart`: introduce `customerAuthor` constant (mirroring
the
API), expose `isFromCustomer` getter, redefine `isFromSupport` as its
inverse. Anything that is not the literal `'Customer'` string (agent
name,
`AutoResponder`, or a defensive `null`) now counts as support.
- `support_chat_message_bubble.dart`: switch the bubble's alignment /
colour
branch on `isFromCustomer`. Render a small `Support` label above the
bubble for non-customer messages so the user can tell agent/bot replies
apart from their own.
- `assets/languages/strings_{de,en}.arb`: add `supportChatSupportLabel`
(alphabetically inserted between `supportChat` and
`supportCreateTicket`).
- Model + widget tests rewritten with realistic wire values
(`'Customer'`, `'Robin'`, `'AutoResponder'`) — the previous fixtures
(`'alice'`, `'user-1'`, `null`) hid the bug.
## Test plan
- [x] `flutter analyze` clean on touched files
- [x] `flutter test test/screens/support
test/packages/service/dfx/models/support` — 73 tests pass
- [x] `dart run tool/generate_localization.dart` regenerates
`lib/generated/i18n.dart` (gitignored)
- [ ] Manual: open a ticket in the app, send a message, confirm the
AutoResponder reply now lands on the left with the `Support` label
- [ ] Manual: confirm a real agent reply renders identically (left,
grey,
`Support` label)
…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\`.
) Companion app PR to [DFXswiss/api#3772](DFXswiss/api#3772) (merged 2026-05-26). **Closes V9** in [\`docs/api-authority-audit.md\`](docs/api-authority-audit.md). The Support tile now reads the new \`user.capabilities.createSupportTicket\` field for its tap decision. No more local \`mail != null\` reconstruction; the rule lives on the backend, the app maps a typed enum value to a UI step. ## Architecture Per the eight consumer rules in [\`CONTRIBUTING.md\`](CONTRIBUTING.md) → "Consuming API capabilities — eight rules" (documented in [#593](#593)): | State | Tap action | |---|---| | \`capability == null\` (legacy backend pre-#3772) | Direct push to Support — API is the authority | | \`capability.available == true\` | Direct push to Support | | \`capability.available == false, missingPrerequisite == email\` | Push email capture page; on \`pop(true)\` re-init the cubit and push Support if the refreshed capability is now available (or null — symmetric to branch 1) | | \`missingPrerequisite == unknown\` or \`null\` | Defensive direct push — let the API render the error | \`MissingPrerequisite\` is an **open enum** with \`email\` + \`unknown\`. Additive backend values degrade to \`unknown\` so a future prerequisite type never breaks \`/v2/user\` parsing for unrelated callers (KYC, settings, etc.). ## What changes ### \`lib/\` - \`packages/service/dfx/models/user/dto/user_dto.dart\` — \`UserCapabilitiesDto.createSupportTicket\` optional field + \`CreateSupportTicketCapabilityDto\` + open enum \`MissingPrerequisite\`. - \`screens/settings_contact/settings_contact_page.dart\` — BlocProvider-wrapped; Support tile \`onTap\` dispatches through \`_onSupportTap\` (4 branches above). Tile layout unchanged. - \`screens/settings_contact/cubit/...\` — new \`SettingsContactCubit\` + state (\`part of\` pattern, States extend Equatable). - \`screens/support/cubits/support_email_capture/...\` — new cubit + state for the standalone email capture flow. - \`screens/support/subpages/support_email_capture_page.dart\` — standalone page (no KYC coupling) calling \`RealUnitRegistrationService.registerEmail\`. \`mergeRequested\` status surfaces a dedicated message — the multi-step verification flow is deliberately not dragged into this minimal page. - \`setup/routing/routes/support_routes.dart\` + \`setup/routing/router_config.dart\` — new \`SupportRoutes.emailCapture\` under \`/support/email\`. - \`assets/languages/strings_{en,de}.arb\` — 4 new keys (alphabetically sorted, both languages). - \`lib/generated/i18n.dart\` — regenerated via \`dart run tool/generate_localization.dart\`. ### \`test/\` | File | Tests | |---|---| | \`packages/service/dfx/models/user/dto/user_dto_test.dart\` | +14 cases for createSupportTicket parsing, incl. \`unknown\` degradation and JSON-null handling | | \`screens/settings_contact/cubit/settings_contact_cubit_test.dart\` | 6 cases | | \`screens/settings_contact/cubit/settings_contact_state_test.dart\` | 8 cases | | \`screens/settings_contact/settings_contact_page_test.dart\` | 15 widget tests covering tile visibility + 11 routing branches incl. \`unknown\` and pop(null\|false) | | \`screens/support/cubits/support_email_capture/support_email_capture_cubit_test.dart\` | 5 cases (success, mergeRequested, ApiException, generic throw) | | \`screens/support/cubits/support_email_capture/support_email_capture_state_test.dart\` | 7 Equatable cases | | \`screens/support/subpages/support_email_capture_page_test.dart\` | 9 widget tests | | \`goldens/screens/support/support_email_capture_golden_test.dart\` | 2 goldens (default + submitting) | | \`goldens/screens/settings_contact/settings_contact_golden_test.dart\` | Re-baselined for BlocProvider wrap; visual surface unchanged | ## Local verification - \`dart format\` clean on all touched files - \`dart analyze lib/ test/\` — no issues found - \`flutter test\` on touched scope → **140/140 green** ## Review history Implemented + reviewed via internal subagent loop, three iterations: 1. First implementation off \`develop\` → had to rebase onto \`chore/post-580-followups\` (PR #588 base mismatch). 2. Reviewer found 2 SHOULD-FIX (Branch-1 asymmetry with \`?? false\` violating the no-fallback rule; \`MissingPrerequisite.fromString\` throw breaking unrelated \`/v2/user\` callers on additive backend changes). Both addressed: explicit null-check symmetry, open-enum \`unknown\` degradation. 3. Final reviewer pass found a \`dart format\` issue on the enum block — fixed; added an explicit \`unknown\`-routing widget test as a NICE-TO-HAVE. ## Targeting \`chore/post-580-followups\` Per request — this is a post-#580 follow-up that consumes a new API capability rather than introducing one in isolation. PR base set accordingly. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary Integration branch bundling the RealUnit registration UX improvements so they can land on `develop` as a single coordinated change. Individual feature PRs target this branch as their base; once they have all merged here, this PR is reviewed end-to-end and merged into `develop`. ## Member PRs - **[#599](#599) — feat(i18n): broaden purchase pre-flight message wording** Renames the buy-screen banner from "Registrierung erforderlich" → "Zusätzliche Angaben erforderlich" so KYC-completed users aren't told they need to register again. - **[#600](#600) — feat(realunit): pre-fill registration form from wallet status** Pre-populates the Aktionariat registration wizard from `GET /v1/realunit/wallet/status`, replacing the empty-controllers experience that today forces users to retype data the backend already holds. Pairs with [DFXswiss/api#3782](DFXswiss/api#3782). ## Why bundle Both PRs touch the same screen (RealUnit registration), share goldens, and only deliver their full UX value together — the new wording communicates the right expectation, and the pre-fill makes that expectation accurate. Reviewing them as one end-to-end change at the integration step prevents partial-merge regressions and keeps the goldens in sync. ## Workflow 1. Open each member PR against `integration/realunit-registration` 2. Merge member PRs into this branch as they're approved 3. Review this PR end-to-end against `develop` (full diff = combined member content) 4. Merge into `develop` Empty seed commit will disappear once the member PRs merge in and the branch diverges with real content. --------- Co-authored-by: Blume1977 <jana.ruettimann@dfx.swiss> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…wallet-status feat(realunit): pre-fill registration form from wallet status
## Summary Adds an intermediate `staging` lane between feature branches and `develop`, mirroring the existing `develop -> main` automation. After this lands: 1. Feature PRs target `staging` (not `develop`). 2. A merge into `staging` triggers `auto-staging-pr.yaml`, which opens a `staging -> develop` PR — analogous to `auto-release-pr.yaml`'s `develop -> main` flow. 3. Merging that PR into `develop` keeps the existing `develop -> main` release flow intact. ## What changes - **New** `.github/workflows/auto-staging-pr.yaml` — copy of `auto-release-pr.yaml`, retargeted to `staging -> develop`. Opens a single open PR with a commit-count + checklist, idempotent. - **Modified** `.github/workflows/pull-request.yaml` — adds `staging` to the `push:` trigger so post-merge verification fires the moment a feature merge lands on `staging`. The existing `pull_request: branches-ignore: [main]` already covers PRs targeting `staging`, so feature → staging PRs already run the build with no further trigger change. ## Workflows already covering staging without changes | File | Why it already covers staging | |---|---| | `bitbox-simulator.yml` | `branches-ignore: [main]` on `pull_request` | | `tier3-handbook.yaml` | `branches-ignore: [main]` on `pull_request` | | `handbook-build-check.yaml` | Runs on every `pull_request` (no branch filter) | ## Workflows intentionally left develop/main only | File | Why | |---|---| | `auto-tag.yaml` | Tags are created from `develop` / `main`. `staging` is not a tag target. | | `handbook-deploy.yaml` | DEV handbook deploys from `develop`, PRD from `main`. `staging` is not a deploy target. | | `release.yaml` | Tag-driven, independent of branch flow. | ## Follow-ups after this merges 1. Push the `staging` branch from `develop` HEAD. 2. Apply the same branch protection to `staging` as `develop` (require CI checks + review). 3. Re-target the in-flight PRs from `develop` to `staging`. ## Test plan - [ ] Branch protection on `staging` matches `develop`. - [ ] After staging is created + a feature PR merges into staging, `auto-staging-pr.yaml` opens the expected `staging -> develop` PR. - [ ] Re-targeted feature PRs (e.g. the ongoing audit fixes) show their `pull_request` CI runs as before.
## Summary Documents the staging branch lane that was wired up by PR #623, so contributors know where to target their PRs. ## What changes - **`CONTRIBUTING.md`**: new "Branch Flow" section right after Build & Test Commands. Names the three branches that participate in the release lane (`staging`, `develop`, `main`), spells out that **feature PRs target `staging`**, links the two auto-promotion workflows, and shows the flow as ASCII. - **`README.md`**: updates the "Typical patch flow" sentence so it starts from `staging` instead of `develop`. ## Note This is the first PR opened against `staging` — it doubles as a smoke test for the new lane. ## Test plan - [ ] After merge into `staging`, `auto-staging-pr.yaml` opens the expected `Promote: staging → develop` PR.
## Summary
`auto-tag.yaml` set the SSH remote to a hardcoded
`git@github.com:DFXswiss/realunit-app.git`. After the recent transfer to
`RealUnitCH/app`, the next tag push would either rely on GitHub's 301
redirect (which `git push` over SSH does not follow reliably) or fail
outright.
Switching to `${{ github.repository }}` resolves to the current
`<owner>/<repo>` of the workflow run, so the remote tracks the repo
automatically across future renames and transfers.
## Verification
- [ ] Workflow YAML still parses (no shell quoting issues with the `${{
}}` substitution).
- [ ] Next push to develop triggers `auto-tag.yaml` and the tag lands
cleanly on the new origin.
## Summary Scopes a `diffThreshold: 0.005` only on the `home_page_loaded` golden via `AlchemistConfig.runWithConfig`. Every other golden stays at the default `0.0` (exact-match). ## Why The home_page_loaded golden renders a stroked price-chart path. Sub-pixel anti-aliasing along that path produces coverage values that jitter across runs even on the locked dfx01 renderer — hardware-lock is not enough for stroked-path AA. Evidence: - `044852b` (28.05.26, bot regen) refreshed `home_page_loaded` *and* `buy_registration_required` - `d25c61d` (01.06.26, #620) refreshed `home_page_loaded` again — only 4 days later, no UI change in scope - Pixel diff between the two `home_page_loaded` versions: **0.24% of pixels** changed, clustered in the chart-curve region only (rows 129–244, cols 252–385). Both regens were the *correct* fix in the moment (a real pixel drift had to be reconciled), but they're paying recurring cost for a structural property of the chart widget. ## Why scoped, not global A global `diffThreshold` would weaken the gate for **every** screen — including 93 static-content goldens where 0.0 is exactly right. A small icon removal (~256 px on 329 160-pixel surface) is well under 0.5% and would be silently swallowed. Local scope is the surgical fix: the chart-drift screen tolerates ~0.5%, everything else stays strict. ## Why 0.005 (not lower or higher) - Current chart drift: 0.238% of pixels → 0.005 absorbs it with ~2× headroom for next time Skia jitters slightly more. - Sanity ceiling: any meaningful UI regression in a 390×844 viewport (icon, button, label) covers >2% — well above the threshold. The gate still catches real changes. ## Alternative considered — not chosen Hardening the chart with `StrokeJoin.bevel` + `StrokeCap.butt` in the production code would remove drift at the source, but at the cost of making the chart visually less smooth for end users to satisfy a test concern. Wrong direction. ## Verification - `flutter analyze test/goldens/screens/home/home_golden_test.dart` → No issues found. - Threshold is read at goldenTest registration time via `Zone.current` (see `alchemist/src/golden_test.dart:163`), so the zone-based override correctly reaches the comparator's `variantConfig.diffThreshold` (`golden_test.dart:195`). - The wrapper `goldenTest` from `test/helper/golden_test_with_assets.dart` is preserved (alchemist import uses `show AlchemistConfig, PlatformGoldensConfig` to avoid ambiguous import on `goldenTest`).
## Problem The Tier-3 \`Build iOS simulator app\` step failed on PR #626 with: \`\`\` Swift Compiler Error (Xcode): File '/Applications/Xcode_16.4.app/.../iPhoneSimulator18.5.sdk/usr/lib/swift/shims/module.modulemap' has been modified since the module file '.../ModuleCache.noindex/.../SwiftShims-2ZSRUUIS75TOI.pcm' was built: mtime changed (was 1779274009, now 1779861061) \`\`\` The cached \`SwiftShims-*.pcm\` referenced a previous SDK's \`module.modulemap\` mtime — its own \`module.modulemap\` had been rewritten by the runner image's Xcode upgrade between two macOS-latest runs. ## Root cause \`Cache iOS DerivedData + Pods\` keyed only on \`runner.os + lock files\`. \`runner.os\` is just \`"macOS"\` regardless of the runtime Xcode/SDK version, so a cache restored across an Xcode bump produces \`.pcm\` files that point at SDK file mtimes from the **previous** toolchain. Swift refuses to use them and the build fails. ## Fix Resolve \`xcodebuild -version\` at runtime and inject it into both the cache key and the restore-keys fallback. The cache now invalidates exactly when the active Xcode rolls, so a hit can only restore objects that were built against the current toolchain. The version string is normalised (\`tr '\n' '-' | sed 's/[^A-Za-z0-9.-]//g; s/-\$//'\`) so \`Xcode 16.4 / Build version 16F6\` becomes a safe cache-key fragment like \`Xcode-16.4-Build-version-16F6\`. ## Validation plan - [ ] On first run after merge, cache misses (new key includes Xcode version that didn't exist in the old cache namespace) → clean build, populates a fresh cache. - [ ] Subsequent runs hit the cache as before, but only across the same Xcode version. - [ ] When the runner image upgrades Xcode next, the cache misses again instead of restoring stale modules — same one-time clean-build cost, no \`mtime changed\` failure.
Promote: staging -> develop
…lane-certificates fix(ios): point Match at RealUnitCH/fastlane-certificates
## 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 Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
…dates (#635) ## Summary Applies three product updates to the wallet-type selection (handbook §02), the software-wallet-terms screen (handbook §03), and the Settings page (handbook §12). The Step 1 / Step 2 onboarding architecture and the Network mode itself stay; only the user-facing surface changes. ## §02 / §03 — wallet-type screen copy Five strings on the wallet-type selection (handbook §02) and the software-wallet-terms screen (handbook §03) are rewritten per the latest product handbook copy. The two-step architecture (Step 1 = Software-Wallet vs BitBox, Step 2 = Create vs Restore vs Debug) is unchanged. | ARB key | Old (DE / EN) | New (DE / EN) | |---|---|---| | `realunitWalletSubtitle` | Verwalten Sie Ihre RealUnit Token kostenfrei und bankenunabhängig. / Manage your RealUnit tokens free of charge and independently of banks. | Kaufen und verwalten Sie RealUnit Aktientoken kostenfrei und bankenunabhängig. / Buy and manage RealUnit stock tokens free of charge and independently of banks. | | `softwareWallet` | Digitale Wallet (App) / Software Wallet | Software-Wallet (App) / Software Wallet (App) | | `softwareWalletSubtitle` | Ich möchte eine neue Wallet für den Handel und die Aufbewahrung der RealUnit Token erstellen. / I would like to create a new wallet for trading and storing RealUnit tokens. | Ich möchte eine neue Wallet erstellen oder meine bestehende Wallet (z.B. Aktionariat) wiederherstellen. / I want to create a new wallet or restore my existing wallet (e.g. Aktionariat). | | `bitbox` | BitBox / BitBox | BitBox Hardware-Wallet / BitBox Hardware Wallet | | `hardwareWalletSubtitle` | Verwahren Sie Ihre RealUnit Aktientoken auf diesem separaten, physischen Gerät (einer "Hardware Wallet") aus der Schweiz. / Store your RealUnit stock tokens on this separate, physical device (a "hardware wallet") from Switzerland. | Ich besitze eine Bitbox02 Nova und möchte RealUnit Token darauf verwahren. / I own a Bitbox02 Nova and want to store RealUnit tokens on it. | Reuse confirmed: the `bitbox` key is only referenced in `lib/screens/welcome/welcome_page.dart:92` as the tile title — the BitBox-disconnected / reconnect strings use separate keys (`bitboxDisconnectedTitle`, `bitboxDisconnectedDescription`, `bitboxReconnect`). The rename does not bleed into other screens. `docs/handbook/de/index.html` §02 description rewritten to match the renamed tiles and the broader software-wallet scope. §03 prose is unchanged — the two-step architecture stays, and its screenshot picks up the new header subtitle automatically via the shared welcome-page golden. ## §12 — hide Network-mode row from production builds The Settings → Network row (Mainnet/Testnet toggle) is a developer affordance — an end-user has no legitimate reason to switch networks, and an accidental tap drops them onto Testnet with broken liquidity and a forced reauth on every subsequent network round-trip. The `SettingOption` that opens `SettingsNetworkPage` is wrapped in a `if (kDebugMode)` collection-if, mirroring the existing pattern in `lib/screens/welcome/welcome_page.dart:125` (the **Address + Signature** debug entry). `SettingsNetworkPage`, `SettingsRoutes.network`, `SettingsBloc` network-mode state, `SettingsRepository.networkMode` — all untouched. Default Mainnet stays active for everyone, debug builds still expose the toggle. ## Goldens Three golden surfaces drift on this PR: - `welcome/goldens/macos/welcome_page_ios.png` (handbook §02) - `welcome/goldens/macos/welcome_page_second_step.png` (handbook §03 — header subtitle pulls the new copy) - `settings/goldens/macos/settings_page_default.png` (handbook §12 — Network row gone) Visual Regression will fail on this PR until the goldens are regenerated on the dfx01 self-hosted runner via the `golden-regenerate.yaml` workflow. Once regenerated and pushed, Visual Regression turns green and the handbook screenshots refresh automatically (the mapping in `scripts/assemble-handbook-screenshots.sh` already points at these three files). ## Out of scope - §01 (welcome splash claim `Bankenunabhängig. Sicher.` → `Sicher. Einfach. Bankenunabhängig.`) — the claim is baked into `assets/images/splash/splash_background.png`; needs a fresh asset from the designer before it can land. ## Test plan - [ ] `dart run tool/generate_localization.dart` regenerates `lib/generated/i18n.dart` with the new strings (already committed). - [ ] `flutter analyze` clean on the touched files. - [ ] Visual Regression: after golden regeneration, `welcome_page_ios.png`, `welcome_page_second_step.png`, and `settings_page_default.png` show the new copy / layout and the handbook screenshots pick them up. - [ ] Production build of the Settings page no longer lists the Network row; debug build still does. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Promote: staging -> develop
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 Release PR
This PR was automatically created after changes were pushed to develop.
Commits: 1 new commit(s)
Checklist