Release: develop -> main#325
Open
github-actions[bot] wants to merge 302 commits into
Open
Conversation
## Summary Stage 48 of the coverage push. Cubit-level coverage of the two-step KYC registration wizard (\`personal\` → \`address\`). ## Cases | Target | Cases | | --- | --- | | initial state | 1 — starts at personal; both steps in order; \`index=0\`; \`progress=0.5\`; \`canGoBack=false\` | | \`next\` | 2 — advances \`personal\` → \`address\` with \`progress=1.0\` and \`canGoBack=true\`; no-op at the last step | | \`previous\` | 2 — goes \`address\` → \`personal\`; no-op at the first step | | \`KycRegistrationStepState\` helpers | 2 — \`progress = (index + 1) / totalSteps\`; \`canGoBack\` is only false on \`personal\` | ## What's pinned - The progress formula uses \`index + 1\` so the bar fills evenly across the two steps (50% / 100%), not by-completion. Pinned via the closeTo check. - Bouncing off the bounds doesn't emit — both edge tests use \`same(before)\` so a future refactor that adds a redundant emit surfaces here. ## Test plan - [x] \`flutter test test/screens/kyc/steps/registration/kyc_registration_step_cubit_test.dart\` — 7 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 49 of the coverage push. Final big sell-flow DTO that wasn't reachable from the payment_dtos aggregate file — it recursively pulls in \`Eip7702Data\` + \`DfxFeesData\` + \`BeneficiaryDto\` + \`PriceStep\`. ## Cases | Target | Cases | | --- | --- | | \`RealUnitSellPaymentInfoDto.fromJson\` | 3 — full happy-path wire shape; nested \`Eip7702Data\` DTO walked all the way down (one leaf per level pinned); integer wire values for \`amount\` / \`exchangeRate\` / \`rate\` widen to double | ## What's pinned - The recursive \`fromJson\` chain actually parses every nested DTO. One leaf field per level (\`relayerAddress\` → \`domain.chainId\` → \`message.delegate\` → \`amountWei\`) is pinned so a regression to "stops at top level" surfaces here. - \`Currency.fromCode\` is the wire→Dart conversion; \`CHF\` round-trips correctly. - Integer-typed JSON values widen to double for the floating-point fields — pins the contract used by partner APIs that sometimes serialize whole-number rates as \`int\`. ## Test plan - [x] \`flutter test test/packages/service/dfx/models/payment/sell_payment_info_dto_test.dart\` — 3 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary
Stage 50 of the coverage push. Small widget test for the \`InfoRow\`
label/value row used across receipts and detail screens.
## Cases
| Target | Cases |
| --- | --- |
| \`InfoRow\` | 3 — renders the leading label with a \`:\` appended and
the trailing value verbatim; default padding is \`EdgeInsets.only(top:
5, bottom: 5)\`; custom padding overrides the default |
## What's pinned
- The trailing colon is a visual contract — pinned via
\`find.text('IBAN:')\` rather than \`find.textContaining('IBAN')\`.
- Default vs override padding is the only configurable behaviour — both
branches covered.
## Test plan
- [x] \`flutter test test/widgets/info_row_test.dart\` — 3 pass
- [x] \`flutter analyze\` clean on the new file
- [ ] CI green
## Summary Stage 51 of the coverage push. Widget tests for the round action button used on the dashboard (Buy / Sell / Receive). ## Cases | Target | Cases | | --- | --- | | \`ActionButton\` | 4 — renders icon + label when not loading (no activity indicator); renders the \`CupertinoActivityIndicator\` and hides icon + label when \`isLoading=true\`; tap fires \`onPressed\` when enabled; tap is disabled while loading (\`InkWell.onTap\` becomes \`null\`) | ## What's pinned - The loading state is **mutually exclusive** with the content: when \`isLoading=true\`, the icon and label are not present in the tree — pinned via \`findsNothing\`. - Tap-while-loading uses \`InkWell\`'s \`null\` onTap rather than just ignoring the callback, so the ink ripple never appears either. Counter-based assertion pins the callback contract. ## Test plan - [x] \`flutter test test/widgets/action_button_test.dart\` — 4 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 52 of the coverage push. Widget tests for the highlight-substring helper used by legal copy and toast snippets. ## Cases | Target | Cases | | --- | --- | | \`TextSubstringHighlighting\` | 6 — substring-not-found fallback (plain \`Text\`, internal \`TextSpan.children\` is null); substring-found splits into three spans (before / match / after); default \`highlightedStyle\` is \`base.copyWith(fontWeight: bold)\`; custom \`highlightedStyle\` replaces the default bold entirely; \`onHighlightedTap\` attaches a \`TapGestureRecognizer\` and firing it triggers the callback; null callback → recognizer is null | ## What's pinned - The fallback vs highlight branch is observable from the rendered tree: the fallback wraps a plain \`Text\` (internal \`RichText.text.children == null\`), the highlight branch builds a \`RichText\` directly with three children. Pin both. - The default highlighting style preserves the base font size (\`copyWith\`) — refactoring the default to a fresh \`TextStyle()\` would break callers that rely on font-size inheritance. - The tap recognizer is **optional**: when \`onHighlightedTap\` is null, no recognizer is attached so the span doesn't intercept touches. ## Test plan - [x] \`flutter test test/widgets/text_substring_highlighting_test.dart\` — 6 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 53 of the coverage push. Widget tests for the privacy-mode amount renderer used across balance + transaction screens. ## Cases | Branch | Cases | | --- | --- | | \`state.hideAmounts=false\` | 4 — zero amount renders placeholder \`'€ --.-- '\`; non-zero renders via \`formatFixed\`; empty \`leadingSymbol\` drops the \`'€ '\` prefix; \`trailingSymbol\` is appended after the amount | | \`state.hideAmounts=true\` | 2 — renders \`'€ ***.**'\` regardless of the amount; empty \`leadingSymbol\` still hides the amount with \`'***.**'\` | ## What's pinned - The zero-amount placeholder is exactly \`'--.-- '\` (with a trailing space from the empty default \`trailingSymbol\`). - Privacy mode (\`hideAmounts=true\`) is a hard override — non-zero amounts and \`trailingSymbol\` are not displayed. - The leading-symbol contract is \`'<symbol> '\` (symbol + single space) when non-empty, and is completely dropped when empty — pinned by both the visible and the absent variant. ## Test plan - [x] \`flutter test test/widgets/hide_amount_text_test.dart\` — 6 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 54 of the coverage push. First cubit-level coverage of the BitBox sell flow now that PR #332 has landed (BitBox sign hardening + \`FakeBitboxCredentials\`). ## Cases | Target | Cases | | --- | --- | | constructor / \`_checkEthBalance\` | 4 — disconnected BitBox → \`SellBitboxBitboxRequired\`; \`ethBalance >= requiredGasEth\` → \`SellBitboxEthReady\`; \`ethBalance < required\` + faucet success → \`WaitingForEth\` (\`faucet.requestFaucet\` called once); faucet throws → \`SellBitboxError\` | | \`proceedToSwap\` | 2 — success: \`Preparing\` → \`AwaitingSwapConfirm\` with both raw txs; failure: \`Error\` | | \`confirmSwap\` | 2 — no-op outside \`AwaitingSwapConfirm\`; non-Bitbox credentials in \`AwaitingSwapConfirm\` → \`Error\` ("BitBox wallet not connected") | | \`confirmDeposit\` | 1 — no-op outside \`AwaitingDepositConfirm\` | | \`retryDeposit\` | 1 — no-op outside \`DepositRetry\` | ## What's pinned - The constructor schedules \`_checkEthBalance\` via \`scheduleMicrotask\`, so all initial-state assertions are made after \`stream.firstWhere((s) => s is! SellBitboxCheckingEth)\`. The fleeting intermediate states (\`CheckingEth\`, \`RequestingFaucet\`) are not asserted but the terminal state is pinned. - The BitBox-required branch reuses \`FakeBitboxCredentials\` from PR #332 with \`bitboxManager = null\` to flip \`isConnected\` to false — same fake that production sign code special-cases. - The non-Bitbox credentials branch in \`confirmSwap\` is the safety net that protects users on a software wallet from accidentally entering the BitBox sign ceremony. - Sign / broadcast / retry happy paths require a real \`MsgSignature\` round-trip — covered separately by the BitBox signer tests in #332. This file pins the cubit's state machine wiring, not the crypto. ## Excluded (and why) - Sign-and-broadcast happy paths (\`confirmDeposit\` success → \`SellBitboxSuccess\`, \`retryDeposit\` success, broadcast-on-deposit failure → \`DepositRetry\`) — these need an actual BitBox sign result threaded through the AppStore wallet chain and a working \`sellService.broadcastTransaction\` mock; deferred to a follow-up stage if needed. ## Test plan - [x] \`flutter test test/screens/sell_bitbox/sell_bitbox_cubit_test.dart\` — 10 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
) ## Summary Stage 55 of the coverage push. First service-level coverage of the registration HTTP service now that PR #332's BitBox sign hardening + \`FakeBitboxCredentials\` are in develop. ## Cases | Target | Cases | | --- | --- | | \`registerEmail\` | 3 — happy path lowercases the email + carries the bearer token; 202 Accepted treated as success; 4xx throws \`ApiException\` | | \`completeRegistration\` | 1 — disconnected BitBox throws \`BitboxNotConnectedException\` before the signing ceremony runs | | \`registerWallet\` | 1 — disconnected BitBox throws \`BitboxNotConnectedException\` before the signing ceremony runs | ## What's pinned - Email is **lowercased** before going on the wire — pinned because the backend treats e-mails case-insensitively and a refactor that drops \`.toLowerCase()\` would create duplicate-account ghosts. - Both \`completeRegistration\` and \`registerWallet\` short-circuit with a typed \`BitboxNotConnectedException\` BEFORE they touch the signer. This is the contract the UI relies on to surface a "connect your BitBox" prompt without a wasted sign attempt. - Bearer-token plumbing comes from \`sessionCache.authToken\` — pinned via header assertion in the happy path. ## Excluded (and why) - Sign-and-post happy paths for \`completeRegistration\` / \`registerWallet\` — would need an end-to-end EIP-712 roundtrip with \`FakeBitboxCredentials(success)\` + a stub server that validates the recovered signer. Deferred to a follow-up stage; the BitBox sign code itself is covered by PR #332's \`eip712_signer_bitbox_test.dart\`. ## Test plan - [x] \`flutter test test/packages/service/dfx/real_unit_registration_service_test.dart\` — 5 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 56 of the coverage push. HTTP-surface coverage of the sell payment-info service now that PR #332 has landed. Skips sign-heavy \`confirmPayment\` in favour of the routes the BitBox flow uses. ## Cases | Target | Cases | | --- | --- | | \`getPaymentInfo\` | 3 — 200 → parsed \`SellPaymentInfo\` + body carries \`amount\` + \`iban\` + \`currency\` code; 403 → \`ApiException\`; 500 → \`ApiException\` | | \`createUnsignedTransactions\` | 2 — 200 → parsed swap + deposit; path is \`/v1/realunit/sell/<id>/unsigned-transactions\`; 500 → \`ApiException\` | | \`broadcastTransaction\` | 2 — 201 → returns \`txHash\`; path is \`/v1/realunit/sell/<id>/broadcast\`; 500 → \`ApiException\` | | \`confirmPaymentWithTxHash\` | 1 — PUTs \`/confirm\` with ONLY the \`txHash\` payload (no \`eip7702\` envelope) | ## What's pinned - All three id-based endpoints embed the id in the URL — pinned via path assertions so a refactor to a query-param shape surfaces here. - \`getPaymentInfo\` carries \`amount\` + \`iban\` + \`Currency.code\` on the wire — the BitBox SellBitboxCubit relies on this contract. - \`confirmPaymentWithTxHash\` is the BitBox-flow shortcut: it sends ONLY the txHash branch of \`RealUnitSellConfirmDto\`, no EIP-7702 envelope. Pinned via \`body.containsKey('eip7702')\` negative. ## Excluded (and why) - \`confirmPayment\` (EIP-712 + EIP-7702 sign roundtrip) — would need a full \`FakeBitboxCredentials(success)\` sign flow with both \`signDelegation\` and \`signAuthorization\`. Deferred; signer correctness itself is covered by the EIP-7702 signer tests in develop. ## Test plan - [x] \`flutter test test/packages/service/dfx/real_unit_sell_payment_info_service_test.dart\` — 8 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
…ests) (#381) ## Summary Stage 57 of the coverage push. Drives the \`SellBitboxCubit\` through the real \`FakeBitboxCredentials(success)\` sign ceremony to cover the post-#332 \`confirmSwap\` / \`confirmDeposit\` / \`retryDeposit\` happy paths that the cubit-test in PR #378 had deferred. ## Cases | Target | Cases | | --- | --- | | \`confirmSwap\` | 2 — signs swap tx → \`AwaitingDepositConfirm\` carries the signed envelope with the raw tx byte-for-byte + 32-byte r/s pair, \`signCallCount=1\`; strips optional \`0x\` prefix before hex-decoding the raw tx | | \`confirmDeposit\` | 2 — signs deposit, broadcasts swap then deposit, calls \`confirmPaymentWithTxHash\` with the deposit txHash, \`signCallCount=2\`; deposit-broadcast failure → \`SellBitboxDepositRetry\` carrying both signed envelopes + the error message | | \`retryDeposit\` | 2 — successful retry emits \`SellBitboxSuccess\` (broadcast count reaches 3); a retry that throws again stays in \`DepositRetry\` with the new \`errorMessage\` | ## What's pinned - The cubit hex-decodes the raw transaction whether it starts with \`0x\` or not — pinned via the mixed-prefix test so a regression to a strict prefix check surfaces here. - \`confirmDeposit\` issues TWO broadcasts (the already-signed swap + the freshly-signed deposit) before calling \`confirmPaymentWithTxHash\`. The first broadcast's failure is **not** retried — pinned by the call ordering. - \`DepositRetry\` carries both signed envelopes verbatim so \`retryDeposit\` can re-broadcast without re-signing — pinned by checking that \`signCallCount\` stays at 2 across the retry. - The signed (r, s) pair always comes back as a 0x-prefixed 32-byte hex string (66 chars including the prefix) — pinned via length assertion, which catches an off-by-one padding regression. ## Test plan - [x] \`flutter test test/screens/sell_bitbox/sell_bitbox_cubit_happy_paths_test.dart\` — 6 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
…py paths (+2 tests) (#384) ## Summary Stage 60 of the coverage push. Drives the two BitBox-aware registration endpoints with a real \`EthPrivateKey\` credential so the EIP-712 signing chain + ASCII transliteration are exercised end-to-end (Stage 55 only covered the BitBox-disconnected guard). ## Cases | Endpoint | What's pinned | | --- | --- | | \`completeRegistration\` | POST to \`/v1/realunit/register/complete\`; envelope is ASCII-transliterated (\`Adä → Adae\`, \`Zürich → Zuerich\`, email lowercased); 65-byte EIP-712 signature; \`kycData\` keeps the ORIGINAL diacritics (\`Adä\`, \`Loveläce\`, \`Zürich\`, \`Bahnhofstraße\`) so ID-verification sees the legal name | | \`registerWallet\` | POST to \`/v1/realunit/register/wallet\` with \`walletAddress\` in EIP-55 form, a 65-byte signature, and a YYYY-MM-DD \`registrationDate\` | ## What's pinned - The **split treatment** of personal data is the load-bearing invariant: the SIGNED envelope copy is ASCII-transliterated (because BitBox firmware rejects non-ASCII), but \`kycData\` keeps the original diacritics so the ID-verification provider sees the legal name. Pinned by asserting both halves of the body. - Email gets \`toLowerCase()\` + \`toBitboxSafeAscii()\` — \`AdA@ExAmPlE.COM\` → \`ada@example.com\`. - Signature length pin (132 chars including \`0x\`) catches a regression that drops the trailing \`v\` byte. ## Test plan - [x] \`flutter test test/packages/service/dfx/real_unit_registration_service_happy_test.dart\` — 2 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
…s) (#382) ## Summary Stage 58 of the coverage push. Drives every negative branch of \`_validateEip7702Data\` via \`confirmPayment\` so a regression in the guard surfaces here instead of in a real signing flow. ## Cases | Branch | Trigger | | --- | --- | | MetaMask Delegator mismatch | wrong \`delegatorAddress\` | | Delegation manager mismatch | wrong \`delegationManagerAddress\` | | Verifying contract mismatch | wrong \`domain.verifyingContract\` | | Wallet mismatch | \`message.delegator != walletAddress\` | | Chain ID mismatch | \`domain.chainId != expectedChainId\` | | Relayer mismatch | \`message.delegate != relayerAddress\` | | Token mismatch | \`tokenAddress != RealUnit asset address\` | | Amount mismatch | \`amountWei != userAmount * 10^decimals\` | | Amount parse | \`amountWei\` is non-numeric | ## What's pinned - MetaMask Delegation Framework v1.3.0 constants (\`_metaMaskDelegator\` + \`_delegationManager\`) are reproduced verbatim in the test file so a refactor that updates one set of constants without the other surfaces here. - The check on \`tokenAddress\` ties the EIP-7702 envelope to \`apiConfig.asset.address\` — the test uses the live mainnet RealUnit asset address (\`0x553C...090B\`) to keep the production constant in scope. - \`amountWei\` is computed as \`userAmount * 10^decimals\`. \`realUnitAsset.decimals == 0\` so the expected wei equals the user amount — the test passes \`userAmount=100\` and \`amountWei='99'\` to trigger the mismatch. - Non-numeric \`amountWei\` falls through the same \`amount mismatch\` branch, not a parser exception. Pinned. ## Test plan - [x] \`flutter test test/packages/service/dfx/real_unit_sell_payment_info_service_validation_test.dart\` — 9 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 62 of the coverage push. Pure-visual contract test for the KYC/registration prompt rendered in the buy flow. ## Cases | Target | Cases | | --- | --- | | \`PaymentActionRequired\` | 2 — renders title + description + \`Icons.info\`; icon carries \`RealUnitColors.realUnitBlue\` at 24px | ## What's pinned - The brand color + icon size is the only behaviour the widget owns — everything else is layout. Pin those to keep the prompt visually consistent across the buy flow. ## Test plan - [x] \`flutter test test/screens/buy/widgets/payment_action_required_test.dart\` — 2 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 63 of the coverage push. Widget tests for the app's shared button primitives used across buy/sell/KYC flows. ## Cases | Target | Cases | | --- | --- | | \`AppTextButton\` | 5 — renders label + triggers \`onPressed\`; icon-less uses plain \`TextButton\`; icon variant renders an \`Icon\` next to the label; \`fullWidth: true\` wraps in a \`SizedBox(width=∞)\`; \`fullWidth: false\` does NOT wrap | | \`AppFilledButton\` | 5 — idle: tap fires \`onPressed\`; loading: \`CupertinoActivityIndicator\` + disabled tap; success: non-tappable; error: non-tappable; icon variant renders the \`Icon\` in idle state | ## What's pinned - \`AppFilledButton\` hard-codes \`onPressed: null\` for the non-idle states (loading / success / error). The three non-idle tap-disabled tests pin this so a future refactor that wires \`onPressed\` through any of those branches surfaces here — users must not be able to re-submit during loading or after success. - The \`fullWidth\` wrapping behaviour is observable from the tree (\`SizedBox(width: double.infinity)\`). Both branches pinned. - The icon vs no-icon split swaps between \`TextButton\` / \`TextButton.icon\` and \`FilledButton\` / \`FilledButton.icon\`. The icon-presence assertion pins both halves. ## Test plan - [x] \`flutter test test/widgets/buttons/app_buttons_test.dart\` — 10 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 69 of the coverage push. Static \`TextSpan\` builder used to embed tappable links inside legal copy. ## Cases | Target | Cases | | --- | --- | | \`TextLinkSpan.link\` | 3 — returns a TextSpan with the text + a \`TapGestureRecognizer\`; defaults to underlined style; custom style override drops the default underline | ## What's pinned - A custom \`style\` REPLACES the default underline rather than merging with it. Pinned via \`decoration\` being null when a custom style without an explicit decoration is supplied — surfaces a regression to a \`.merge\` call. - The recognizer is always attached (the link is always tappable) — the underlying \`_launchLink\` URL behaviour is platform-coupled and stays out of scope here. ## Test plan - [x] \`flutter test test/widgets/text_link_span_test.dart\` — 3 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary
Stage 61 of the coverage push. HTTP + SharedPreferences-backed debug
auth helper that lets the dev-flavor app sign in without going through
the wallet UI.
## Cases
| Target | Cases |
| --- | --- |
| \`savedAddress\` / \`savedSignature\` | 2 — both null when
SharedPreferences is empty; both return the stored values when
pre-populated |
| \`fetchSignMessage\` | 2 — GETs \`/v1/auth/signMessage?address=...\`
and returns the \`message\` field; non-200 → \`Exception\` carrying the
status code |
| \`authenticate\` | 2 — on 201 with \`{accessToken: ...}\`: stores
token in \`sessionCache\`, stashes signature under the EIP-55 checksum
address in \`sessionCache\`, persists raw address + signature to
\`SharedPreferences\`; non-201 → \`Exception\` carrying the status code
|
## What's pinned
- The signature is stashed under the **EIP-55 checksum form** of the
address (not the lowercase variant the user passed). Pinned via
\`session.signatureAddress == '0x9F5713DEacB8e9CAB...'\` after
authenticating with the lowercase address — a refactor that drops the
checksum would surface here.
- The wire body is \`{wallet: 'RealUnit', address, signature}\` — pinned
verbatim so a backend contract change doesn't slip past silently.
- The SharedPreferences keys are \`debugAuthAddress\` /
\`debugAuthSignature\` — string-literal pin so renames don't silently
break cross-session persistence.
## Test plan
- [x] \`flutter test
test/packages/service/debug_auth_service_test.dart\` — 6 pass
- [x] \`flutter analyze\` clean on the new file
- [ ] CI green
## Summary Stage 65 of the coverage push. Drag-based confirm widget used on the buy / sell confirm sheets — slide-to-confirm UX pattern. ## Cases | Target | Cases | | --- | --- | | \`StandardSlideButton\` | 4 — renders the buttonText label; a short drag (< 90% across the track) does NOT fire \`onSlideComplete\`; a long drag (full track) fires \`onSlideComplete\` exactly once; the knob snaps back to the left edge after each drag end | ## What's pinned - The drag completion threshold is \`effectiveMaxWidth - sliderWidth - 10\` (≈ 90% of the track for a 300px host). Pinned via two adjacent drag tests so a regression that fires on partial slides surfaces here. - The knob always snaps back to \`_dragPosition = 0\` after release, regardless of whether the completion fired. This keeps the visual state idempotent across re-confirmations. - Test uses a fixed-width \`SizedBox(width: 300)\` host so the geometry is deterministic. ## Test plan - [x] \`flutter test test/widgets/standard_slide_button_test.dart\` — 4 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 66 of the coverage push. Generic typed selector widgets used across dashboard time-period switching + filter chips. ## Cases | Target | Cases | | --- | --- | | \`TagSelection<String>\` | 4 — one ChoiceChip per item with the right label; marks exactly the chip matching \`selected\`; tapping fires \`onSelected\` with the tuple's value; icon variant renders the IconData when the tuple supplies one | | \`TabSelector<String>\` | 4 — one \`labelBuilder\` result per tab; selected tab's label receives \`isSelected: true\`; alignmentX animates between \`-1.0\` (first) and \`+1.0\` (last) as tabs change; single-tab selector centers the indicator (\`alignmentX = 0\`) | ## What's pinned - \`TabSelector\` computes \`alignmentX = -1.0 + (2.0 * selectedIndex / (tabCount - 1))\` to slide the highlight across the track. Pinned at both extremes (\`-1.0\` first, \`+1.0\` last) and the single-tab edge case (\`tabCount <= 1 → alignmentX = 0\`) so a refactor to a different easing surfaces here. - \`TagSelection\` uses Dart records (\`(T, String, IconData?)\`) — the tuple shape is the public contract. - \`labelBuilder\` is the only state the consumer controls — pinned by inspecting the \`fontWeight\` of the rendered \`Text\`. ## Test plan - [x] \`flutter test test/widgets/tag_tab_selectors_test.dart\` — 8 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 64 of the coverage push. Form-primitive widget tests shared across KYC, settings and bank-account flows. ## Cases | Target | Cases | | --- | --- | | \`LabeledTextField\` | 4 — omits the label when null; renders it when provided; \`initialValue\` seeds the form field; \`onChanged\` fires when the user types | | \`DropdownField<String>\` | 3 — renders all items as menu choices when the dropdown is tapped; renders the label when provided; \`onChanged\` fires when the user picks an item | ## What's pinned - \`LabeledTextField\` conditionally renders the label \`Text\` widget — pinned via the absent-label test so a refactor that always emits the Padding/Text surfaces here. - \`DropdownField<T>\` open-and-pick interaction is non-trivial. Pinned via \`tester.tap(find.byType(DropdownButtonFormField<String>))\` + \`pumpAndSettle\` to give the overlay time to mount. ## Test plan - [x] \`flutter test test/widgets/form/form_fields_test.dart\` — 7 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary
Stage 67 of the coverage push. PIN entry keypad widget.
## Cases
| Target | Cases |
| --- | --- |
| \`NumberPad\` | 5 — renders digit buttons 0-9 + delete (arrow icon);
decimal button rendered ONLY when \`onDecimalPressed\` is provided;
tapping each digit fires \`onNumberPressed(digit)\`; tapping delete
fires \`onDeletePressed\`; tapping decimal fires \`onDecimalPressed\` |
## What's pinned
- The decimal slot is **optional** — when \`onDecimalPressed\` is null,
the slot renders a 60×60 placeholder \`SizedBox\` (no '.' text). Pinned
via \`find.text('.'), findsNothing\` so a refactor that always renders
the dot surfaces here.
- The digit-press callback receives the actual int (\`1\` not \`'1'\`) —
pinned by accumulating to a typed \`List<int>\`.
## Test plan
- [x] \`flutter test test/widgets/number_pad_test.dart\` — 5 pass
- [x] \`flutter analyze\` clean on the new file
- [ ] CI green
## Summary Stage 68 of the coverage push. Two small visual primitives used across the settings + bottom-sheet flows. ## Cases | Target | Cases | | --- | --- | | \`OutlinedTile\` | 4 — renders leading + title; subtitle Text rendered only when provided; trailing icon omitted when \`onTap\` is null; trailing icon present + tappable when \`onTap\` is provided | | \`Handlebars.horizontal\` | 3 — renders with a non-zero default width (defaults to 25% of parent); honours an explicit \`width\` override; honours an explicit \`margin\` override | ## What's pinned - \`OutlinedTile\` ties the trailing chevron to \`onTap != null\` so non-interactive tiles don't render a misleading affordance. Pinned via both branches. - \`Handlebars.horizontal\` is a static helper that returns a styled \`Container\`; overrides on \`width\` + \`margin\` are pinned via direct widget inspection rather than golden testing. ## Test plan - [x] \`flutter test test/widgets/outlined_tile_handlebars_test.dart\` — 7 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 73 of the coverage push. KYC financial-data confirmation checkbox. ## Cases | Target | Cases | | --- | --- | | \`KycQuestionCheckboxWidget\` | 5 — falls back to \`question.title\` when options is null; prefers \`options.firstOrNull.text\` over the title; \`value\` prop sets the checkbox state; tap toggles \`onChanged\` with the new bool; null from the framework is normalised to false | ## What's pinned - The title-fallback path matters for free-text checkbox questions (no options), the options-text path matters for label-styled options. Both branches pinned. - \`Checkbox.onChanged\` can fire with null (tristate clear); the widget normalises to \`false\` so the cubit never sees null. Pinned via direct invocation of the captured callback. ## Test plan - [x] \`flutter test test/screens/kyc/steps/financial_data/widgets/kyc_question_checkbox_widget_test.dart\` — 5 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 74 of the coverage push. KYC financial-data single-choice radio list. ## Cases | Target | Cases | | --- | --- | | \`KycQuestionSingleChoiceWidget\` | 3 — renders one ListTile per option with the option text; no options → empty Column (no ListTiles); tapping a ListTile fires \`onChanged\` with the option key | ## Test plan - [x] \`flutter test test/screens/kyc/steps/financial_data/widgets/kyc_question_single_choice_widget_test.dart\` — 3 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 70 of the coverage push. Read-only mnemonic grid used by the seed-display screen. ## Cases | Target | Cases | | --- | --- | | \`MnemonicReadOnlyField\` | 3 — renders all 12 seed words exactly once each; asserts exactly 12 seed words on construction; renders 12 cell \`Text\` widgets | ## Test plan - [x] \`flutter test test/widgets/mnemonic_read_only_field_test.dart\` — 3 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 72 of the coverage push. Dot-row PIN progress indicator used by the setup + verify PIN screens. ## Cases | Target | Cases | | --- | --- | | \`PinIndicator\` | 5 — renders exactly \`expectedPinLength\` dots; fills the first \`pinLength\` dots with the foreground color (transparent thereafter); \`wrongPin=true\`: every border is status-red; \`wrongPin=false\`: every border is black; fully entered pin fills all dots | ## What's pinned - The fill vs transparent split is the only visual signal the user has during PIN entry — pinned across partial, empty, and full input states. - The \`wrongPin\` border swap is the error feedback; pinned at both boolean values to catch a regression that drops the swap. ## Test plan - [x] \`flutter test test/screens/pin/widgets/pin_indicator_test.dart\` — 5 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 71 of the coverage push. Pure value-object tests for the two dashboard chart data points. ## Cases | Target | Cases | | --- | --- | | \`PricePoint\` | 1 — stores asset + price + time as-is, no normalisation | | \`PortfolioValuePoint\` | 2 — stores value + balance + time as-is; \`value\` stays a \`BigInt\` (pinned against a > 64-bit value to catch a refactor to double) | ## What's pinned - \`PortfolioValuePoint.value\` is in rappen/cents per the source doc-comment — pinning the \`BigInt\` type protects very large portfolios from precision loss. ## Test plan - [x] \`flutter test test/models/price_portfolio_points_test.dart\` — 3 pass - [x] \`flutter analyze\` clean on the new file - [ ] CI green
## Summary Stage 75 of the coverage push. Minimal loading placeholder shown while the KYC cubit boots. ## Cases | Target | Cases | | --- | --- | | \`KycLoadingPage\` | 1 — renders an AppBar + centered \`CupertinoActivityIndicator\` | ## Test plan - [x] \`flutter test\` — 1 pass - [x] \`flutter analyze\` clean - [ ] CI green
## Summary Stage 76 of the coverage push. Address-display QR code with tappable copy area for the receive screen. ## Cases | Target | Cases | | --- | --- | | \`QRAddressWidget\` | 4 — renders a \`QrImageView\` for the uri; renders the address chunks (prefix + suffix at minimum); renders a copy icon; address row wrapped in a tappable \`InkWell\` | ## Excluded (and why) - The chunk-split styling (bold prefix + bold suffix at indices \`[0..6]\` + \`[36..]\`) is fiddly to assert through the rendered TextSpan tree because Flutter wraps the spans through several layout passes. The visible chunks are pinned via \`find.textContaining\`; styling is left to future widget-tree-snapshot tests if needed. ## Test plan - [x] \`flutter test\` — 4 pass - [x] \`flutter analyze\` clean - [ ] CI green
## Summary Stage 77 of the coverage push. Loading placeholder shown while the edit-name / edit-address / edit-phone subpages save. ## Cases | Target | Cases | | --- | --- | | \`SettingsEditLoadingPage\` | 2 — renders the passed title in the AppBar; renders a centered \`CupertinoActivityIndicator\` | ## Test plan - [x] \`flutter test\` — 2 pass - [x] \`flutter analyze\` clean - [ ] CI green
## Summary Stage 78 of the coverage push. Two i18n-bearing subpages for the KYC financial-data step (loading + failure). ## Cases | Target | Cases | | --- | --- | | \`KycFinancialDataLoadingPage\` | 1 — renders a centered \`CupertinoActivityIndicator\` | | \`KycFinancialDataFailurePage\` | 2 — renders the passed message; message styled in \`RealUnitColors.status.red600\` | ## Test plan - [x] \`flutter test\` — 3 pass - [x] \`flutter analyze\` clean - [ ] CI green
…) (#703) Reverts [#661](#661) (issue [#660](#660)). ## The bug [#661](#661) relabelled the buy-payment button `buyPaymentConfirm` from *"Click here once you have made the transfer"* to *"Request payment instructions by email"* / *"Zahlungsanweisungen per E-Mail anfordern"*, on the premise that the click *requests* the payment slip by email and the customer pays afterwards. That premise is wrong. On the screen where this button lives (`PaymentInformationDetails`): | Element | Content | |---|---| | `buyPaymentInformationDescription` (above) | "Please transfer the purchase amount using your banking app with these details. The purpose of payment is important!" | | On screen | IBAN, BIC, name, address **and the QR payment request** — the instructions are **already shown** | | Button (after #661) | "Request payment instructions by email" | | `PaymentExecutedSheet` (after tap) | "Thank you. As soon as your transfer has been received, we will transfer the REALU tokens…" | The button's `onPressed` calls `confirmPayment()` → `PUT /v1/realunit/buy/{id}/confirm`, which **confirms the order** and returns a reference. It does not email any instructions, and the instructions are not delivered by email — they are rendered on the same screen. The new label therefore contradicts both the description above it and the success sheet below it, and mislabels a confirm action as an info request. ## The fix Full revert of [#661](#661). Restores the original label, which is consistent with the surrounding copy and the actual `/confirm` behaviour: | Lang | Restored value | |---|---| | de | `Klicken Sie hier, sobald Sie die Überweisung getätigt haben` | | en | `Click here once you have made the transfer` | ## Scope - `buyPaymentConfirm` value in `strings_de.arb` + `strings_en.arb` (key position unchanged, no other keys touched). - Reverts the `home_page_loaded.png` golden that [#661](#661) had bundled in. Goldens are regenerated authoritatively on the dfx01 runner via `golden-regenerate.yaml` after push (the `buy_payment_info_loaded` golden must pick up the restored label). - `lib/generated/i18n.dart` is git-ignored and regenerated in CI. Draft until regenerated goldens land and CI (Analyze & Test + Visual Regression + Coverage Floor) is green. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary Backend [DFXswiss/api#3836](DFXswiss/api#3836) drops `KycRequired` from `RealUnitRegistrationState` (YAGNI — it is no longer emitted by `GET /v1/realunit/registration`). This removes the now-unreachable consumer-side handling so the app stops carrying dead code for a state the API never sends. ## Changes - `real_unit_registration_state.dart` — remove the `kycRequired(jsonName: 'KycRequired')` enum value. - `kyc_cubit.dart` — remove the `case RealUnitRegistrationState.kycRequired` dispatch arm. - `kyc_state.dart` — remove the `KycRequiredFailure` state class. - `kyc_page_manager.dart` — remove the `KycRequiredFailure()` → `KycFailurePage` switch arm. - `real_unit_registration_info_dto.dart` — drop the stale `kycRequired` mention from the doc comment. - `strings_de.arb` / `strings_en.arb` — remove the unused `kycRequiredFailureMessage` key. - `docs/wallet-modes.md` — drop the registration `KycRequired` references. - Tests — remove the obsolete `KycRequired` parse test and the `KycRequiredFailure` cubit test. ## Note `PaymentInfoError.kycRequired` (buy/sell) and the transaction-status `kycRequired` are unrelated concepts and remain untouched. After this change, an unexpected `state: 'KycRequired'` payload would throw `ArgumentError` (covered by the existing unknown-state test) — acceptable since the API no longer emits it. ## Test plan - [x] `flutter analyze` — clean (only a pre-existing, unrelated generated-i18n warning present on `staging`) - [x] `flutter test` — 2272 non-golden tests pass - [ ] Visual Regression / goldens via CI (no golden references the removed state)
Promote: staging -> develop
fix(android): publish releases to Open testing track (not internal)
Promote: staging -> develop
## Problem
A BitBox (hardware) wallet could be persisted with an **empty on-chain
address**. On the next app launch — after PIN entry — the dashboard
build reads that address through `EthereumAddress.fromHex("")`, which
throws. In release this is uncaught in the build phase and surfaces as a
**bare grey screen** (the default `ErrorWidget`). Software wallets are
unaffected because they always derive a real address.
## Root cause
`bitbox_flutter`'s `getETHAddress` coerces a native `null` into `""` at
the transport boundary (`bitbox_usb_method_channel.dart` → `return
result ?? '';`; the iOS/Android handlers return it unvalidated). When
the device isn't fully ready (e.g. a transient BLE stall right after
channel-hash verify), the address comes back empty, and
`createBitboxWallet` persisted it with **no validation** — this gap has
existed since BitBox support was first added. The pairing ceremony also
fetched the address with no device-ready re-check or retry (unlike the
existing channel-hash retry loop).
## What changed
- **Central retrying boundary** — `BitboxService.getEthAddress` never
returns empty: a transient empty read self-recovers across bounded
retries; a persistent one throws the new typed
`BitboxAddressUnavailableException`.
- **Validate before persist** — `createBitboxWallet` (and the heal path)
route through that boundary and keep a format guard, so an empty/invalid
address can never land on disk again. A failed fetch falls back to the
pairing flow's existing retry path.
- **Self-heal for already-corrupted wallets** — at load, a BitBox row
with an empty/invalid address is detected (non-throwing) and the app
diverts to a re-pairing recovery page that re-derives and backfills the
address onto the existing row, then continues to the dashboard. This is
local key derivation (no API state). Cancelling removes the unusable
view-wallet (keys live on the device; re-pairing re-derives the same
address) so the user is never stranded.
- **Defense-in-depth** — a custom `ErrorWidget.builder` replaces the
silent grey box with a logged, on-brand surface, and routes uncaught
build errors through `FlutterError.onError`.
## Test plan
- [x] `flutter analyze` clean
- [x] `flutter test --exclude-tags golden` — full suite green (2334
tests)
- [x] Unit: `createBitboxWallet` rejects empty/invalid without
persisting; `getEthAddress` retry (first-ok / empty-then-ok via
`fakeAsync` / persistent-empty throws);
`currentWalletNeedsAddressRecovery` matrix; `healCurrentBitboxAddress`
happy + throw
- [x] Bloc: `HomeBloc` diverts to recovery and clears the flag after a
clean load
- [x] Widget: recovery `onCancel` does not throw on a single-entry stack
(+ regression guard)
- [ ] On-device: re-pair an empty-address BitBox wallet → lands on
dashboard; cancel → onboarding
## 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>
## Summary Bumps the `bitbox_flutter` dependency from `v0.0.7` to `v0.0.8`. `v0.0.8` contains the Android pairing-code fix (DFXswiss/bitbox_flutter#26): `initBitBox` now runs off the serial MethodChannel task queue, so the BitBox pairing code (channel hash) appears in the app and on the device **simultaneously**, instead of only after confirming on the device. iOS was already unaffected. ## Test plan - [ ] `flutter pub get` resolves `bitbox_flutter v0.0.8` - [ ] Android BitBox pairing: pairing code shows in-app at the same time as on the device (before on-device confirmation) - [ ] Existing BitBox flows (connect / sign) unaffected
## 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>
fix(home): catch recovery-gate failure + pin error surface in a test
## Summary
Fixes the post-account-merge "Wallet registration not complete" error
(red banner) in the RealUnit app, the CONTRIBUTING-aligned way.
When a new wallet signs up with an email that already belongs to an
existing **DFX** account (account merge) and that account has **no prior
RealUnit registration**, `GET /v1/realunit/registration` returns
`state=NewRegistration`. The email-verification step, however, called
`registerWallet` (`POST /register/wallet`) **unconditionally** — that
endpoint only *adds* a wallet to an **existing** registration, so the
API returns `400 "No RealUnit registration found"`.
## Root cause — runtime-confirmed (PROD App Insights)
- `GET /v1/realunit/registration` → **200** (state=NewRegistration,
userData present from existing KYC)
- `POST /v1/realunit/register/wallet` → **400 `{"message":"No RealUnit
registration found"}`** (×4 retries)
- recovery: the user reached the KYC registration form → `POST
/v1/realunit/register/complete` → **201**
- DB: the merged account had no `RealUnitRegistration` step until
`register/complete` created it. 7-day breadth: 7× this 400 (affects
every merge into a DFX account without a prior RealUnit registration).
## Fix (app-only) — single source of registration routing
The email-verification flow is reduced to its actual job: **confirm the
merge** (detect the JWT account change) and hand back to the KYC flow.
`KycCubit` is now the only place that interprets the registration
`state` and routes it (addWallet → link wallet, NewRegistration → full
registration form, AlreadyRegistered → forward) — per CONTRIBUTING.md
"API as Decision Authority". This removes the duplicated, unconditional
`register/wallet` call.
Dead code removed accordingly: `_completeRegistration`,
`_mergeDetected`, the `RealUnitRegistrationService` dependency, the
`KycEmailVerificationRegistrationFailure` state, and the now-unused i18n
key `registerEmailVerificationRegistrationFailed` (de + en).
## Tests
- email cubit (simplified): same account → Failure (link not visited);
changed account → Success (merge confirmed; no registration here); retry
(Failure → Success).
- `kyc_step_states_test` updated for the removed state.
## Test plan
- [x] `flutter analyze` clean
- [x] `flutter test --exclude-tags golden` — full suite passes (2312)
- [x] Coverage Floor Gate replicated locally — scoped **lines 100.0%**
(floor 100)
- [ ] DEV end-to-end: merge into a DFX account without RealUnit
registration → no red error → lands on the registration form →
register/complete
Supersedes #711 (minimal variant) and the earlier incorrect
MergeProcessing PRs DFXswiss/api#3848 + #709.
## 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>
fix(auth): build sign-in message locally instead of fetching it
) ## Why Google Play flags the app bundle: its native code is not 16 KB page aligned, so it cannot run on Android 15+ devices that use 16 KB memory pages. The only offending library was `libgojni.so` (the gomobile-built Go binding shipped by `bitbox_flutter`) — its arm64-v8a `PT_LOAD` segments were `p_align = 0x1000` (4 KB). Every other native lib in the bundle (libflutter, libapp, libsqlite3mc, libtensorflowlite_jni, …) is already ≥ 16 KB. ## What Bump `bitbox_flutter` `v0.0.8 → v0.0.9`. v0.0.9 rebuilds the gomobile binding with `-extldflags=-Wl,-z,max-page-size=16384`, producing 16 KB-aligned `.so` libraries. See DFXswiss/bitbox_flutter#30. `pubspec.lock` updated to the v0.0.9 commit (`6172a2e`); no other lock entries change. ## Verification - `bitbox_flutter` v0.0.9 `jni/arm64-v8a/libgojni.so` + `x86_64`: `p_align` `0x1000` → **`0x4000`** (16 KB) - API-safe bump: javap public signatures (146 lines) and exported dynamic symbols (284) are **byte-identical** between v0.0.8 and v0.0.9 — only ELF alignment changed, so no source-level impact - `bitbox_flutter` PR gate green (go vet/test, dart format, analyze, flutter test) ## Test plan - [ ] App CI green (Analyze & Test, Visual Regression, Coverage Floor Gate) - [ ] Release build: confirm `lib/arm64-v8a/libgojni.so` reports 16 KB alignment in the final AAB and the Play Console warning clears
Promote: staging -> develop
…llet fix(kyc): open BitBox connect sheet when linking shareholder wallet
…siness relationship" (#721) ## Summary The account action in **Settings** opens a logout flow but was labelled **"Terminate business relationship"** (DE: *"Geschäftsbeziehung beenden"*), implying account closure rather than signing out. Relabel it to **"Logout" / "Abmelden"**, consistent with the wording already used throughout the confirm sheet (`realunitWalletLogout`, `logout`). ## Why The settings entry triggers `SettingsConfirmLogoutWalletSheet` → `DeleteCurrentWalletEvent`: a local logout that removes the wallet from the device (hence the recovery-phrase-backup acknowledgement the sheet forces before the action). It does **not** terminate the DFX business relationship, so the previous label was misleading and caused tester confusion. Only the user-facing string changes. The i18n key (`settingsDeleteWallet`) and all logic are intentionally untouched to keep the change focused. ## Changes - **App string** — `assets/languages/strings_{de,en}.arb`: `"Geschäftsbeziehung beenden"` → `"Abmelden"`, `"Terminate business relationship"` → `"Logout"`. - **Visual regression** — settings page Goldens regenerated on the dfx01 runner (`settings_page_default.png`, `settings_page_bitbox.png`); an unrelated ~5 B sub-pixel drift on the home baseline from the regen run was reverted to keep the diff scoped. - **Handbook** — `docs/handbook/de/index.html`: synced the three references to the renamed entry (settings list, screenshot alt text, confirm-sheet description). - **E2E** — `.maestro/handbook/24-settings-delete-wallet.yaml`: retargeted the tap selector from `.*Geschäftsbeziehung beenden.*` to `.*Abmelden.*` so the tier-3 handbook flow keeps finding the entry (still gated on the confirm sheet not yet being visible). ## Test plan - [ ] `Analyze & Test` green - [ ] `Visual Regression` green (settings page baselines reflect the new label) - [ ] `Coverage Floor Gate` green - [ ] `Handbook Build Check` green - [ ] Manual: Settings → entry reads "Abmelden"/"Logout" → opens the logout confirm sheet → logout works --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary After entering the 6-digit PIN, the app showed **no loading feedback** while it (a) ran the off-thread iterated PIN hash and (b) decrypted + loaded the wallet before navigating to the home screen. On Android this gap is noticeable and looks like a frozen screen. This PR adds a spinner with a **"Signing in…" / "Anmeldung…"** label that covers both phases. ## Why Reported in tester feedback: after PIN entry on Android, it takes a relatively long time until the next screen appears, with no spinner or anything indicating the app is working in the background. ## How - New `VerifyPinVerifying` state, emitted at the **start** of `checkPin()` (before the `verifyPin` hash) carrying the entered PIN. - The view treats `VerifyPinVerifying || VerifyPinSuccess` as "loading": the number pad is replaced by a centered `CupertinoActivityIndicator` + label, and the PIN dots stay filled so the screen doesn't look reset. `VerifyPinSuccess` keeps the spinner up through the post-success wallet load while this screen is still on top (navigation only happens once `isLoadingWallet` clears). ## Changes - `verify_pin_state.dart`: add `VerifyPinVerifying`. - `verify_pin_cubit.dart`: emit it before the hash check. - `verify_pin_page.dart`: swap number pad → spinner while loading; keep dots filled (block sized to the number pad footprint to avoid layout jump). - i18n: `pinVerifying` = "Anmeldung…" / "Signing in…". - Tests: cubit sequence (Verifying → Success), state equality, two widget tests; new golden `verify_pin_page_verifying` (regenerated on dfx01, `pumpOnce` for the never-settling spinner). ## Test plan - [ ] `Analyze & Test` green (cubit/state at 100% scoped coverage) - [ ] `Visual Regression` green (new verifying baseline) - [ ] `Coverage Floor Gate` green - [ ] Manual (Android): enter PIN → spinner + "Anmeldung…" appears immediately and stays until the dashboard loads --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Problem Connecting a **brand-new BitBox that has no wallet set up** (no seed) left the user stuck. Pairing succeeds, but the device has no seed to derive an ETH address from, so `getETHAddress` comes back empty. That empty read failed as a generic error → `BitboxNotConnected` → a SnackBar "something went wrong" → and because the re-scan timer is re-armed, the device is immediately found again and the user is walked through the pairing code → fail → SnackBar **loop**, with no hint that the real problem is simply an un-set-up device. (The earlier fix #710 stopped the empty address from being *persisted* — no more grey screen — but did not distinguish "no seed" from a transient empty read.) ## What changed After channel-hash verify, read the device's firmware status via the new `bitbox_flutter` `getDeviceStatus()` (cached read, no device round-trip). When it reports `uninitialized`, emit a dedicated **`BitboxNotInitialized`** state that explains the user must set up / restore a wallet on the device first. - The state offers a **retry** (`recheckDeviceStatus`) that re-reads the status — if the user has since set up a wallet, the connection continues without re-pairing. - It deliberately **does not arm the re-scan timer**, so the silent re-pair loop is gone. - Only `uninitialized` is treated as "no wallet". Other non-ready statuses (e.g. firmware-upgrade-required) intentionally keep the existing failure path rather than being mislabelled. The address-derivation/observe/sign tail of `confirmPairing` was extracted into `_acquireWalletAndConnect()` so the initial flow and the retry share one path. ## Dependency Requires `bitbox_flutter` `getDeviceStatus()` from **[DFXswiss/bitbox_flutter#29](DFXswiss/bitbox_flutter#29. This PR temporarily pins the plugin to that fix branch; it will be moved to the **`v0.0.9`** tag once #29 is merged and tagged. **Kept as Draft until then.** ## Test plan - [x] `flutter analyze` — clean (only the pre-existing generated-`i18n.dart` warning) - [x] `flutter test` — bitbox cubit / service / view suites all green - [x] Cubit: unseeded → `BitboxNotInitialized`, no wallet created, no re-scan loop (state stays stable); retry continues once seeded; retry stays while still unseeded; no-op off-state - [x] Service: `getDeviceStatus` pass-through via the simulator - [x] Widget: `BitboxNotInitialized` renders retry + cancel; retry calls `recheckDeviceStatus` - [x] i18n: new keys in both `de`/`en` ARBs (case-sensitive ASCII order), regenerated - [ ] On-device: pair an un-set-up BitBox → lands on the explanatory screen; set up a wallet + retry → continues to the dashboard
## Summary - Adds one missing test for the `catch` block in `recheckDeviceStatus()` (lines 242–245 in `connect_bitbox_cubit.dart`) - The scenario: device was `uninitialized` on first pair → `BitboxNotInitialized`; user sets up wallet; `recheckDeviceStatus` is called but `createBitboxWallet` throws → cubit must fall back to `BitboxNotConnected` - Fixes Coverage Floor Gate failing at 99.9% (4924/4928 lines) in PR #728 ## Test plan - [ ] `flutter test test/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit_test.dart` passes - [ ] Coverage Floor Gate reaches 100%
## 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> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## What Removes internal references from this public repository so that nothing leaks beyond what is intended for the open codebase. - **Removed two internal planning documents** (an API-authority audit + roadmap) that contained internal-only details not meant for the public repo. - **Repointed stale self-referencing links** to the current repository name (the repo was renamed; the old org/name URLs were carried over from before the move — issue/PR numbers resolve unchanged). - **Genericized internal CI runner hostnames** in docs and workflow comments (now described as "the self-hosted runner" / "self-hosted macOS ARM64 runner"). No functional `runs-on:` labels were changed. - **Replaced a private ops-repo reference** in a workflow comment with a neutral description. ## Scope / safety - Changes are limited to **documentation, prose, comments, and link text**. No application code, test logic, or workflow structure was modified. - All touched workflow YAMLs validate (`yaml.safe_load`). - Edited Dart files (`kyc_cubit.dart`, `kyc_cubit_test.dart`) changed **comments only**. - Public references that are legitimately part of the project (the public DFX API repo, `api.dfx.swiss` hosts, test fixtures) were intentionally **kept**. ## Note This PR scrubs the current tree only. The same content also exists in git history; history was intentionally left untouched (no force-push). If full history scrubbing is desired, that's a separate, heavier operation to schedule deliberately. Opened as a draft for review. --------- Co-authored-by: Josh <joshua.krueger@dfx.swiss> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…737) ## Was Der Kauf-Screen zeigt jetzt nur noch den Betrags-Konverter (oben Input-/Output-Betrag) und einen primären CTA **"Jetzt verbindlich kaufen"**. Ein Klick bestätigt den Kauf verbindlich (`confirmPayment`) und öffnet die neue Seite **"Zahlungsdetails"**. Die Zahlungsdetails-Seite enthält: - Hinweis: "Bitte überweisen Sie den Kaufbetrag mittels Banktransaktion. Die folgende Zahlungsanweisung haben wir Ihnen soeben auch per E-Mail zugestellt." - Hinweis: "Der Verwendungszweck ist wichtig! …" - Die Referenz + vollständige Bankverbindung (IBAN/BIC/Empfänger, Verwendungszweck, optionaler QR-Code) - Abschluss-Button **"Zurück zum Hauptbereich"** → Home ## Änderungen - Neue `BuyPaymentDetailsPage` + Route + `BuyPaymentDetailsParams` - Details-Karte extrahiert in `PaymentDetailsCard` (auf der neuen Seite genutzt) - Neuer `BuyConfirmButton` (confirm → navigieren / Fehler-Snackbar) - `PaymentActionButton` (umbenannt) rendert den CTA im Success-State - `PaymentExecutedSheet` + `PaymentInformationDetails` entfernt (abgelöst) - i18n: `buyPaymentConfirm` umbenannt; neue Keys; ungenutzte entfernt (de+en) - Handbook: neue Seite `53-buy-payment-details` (aus Goldens) + Counts - Tests: neue Widget-/Page-Tests; Buy-Page-/PaymentInfo-Tests angepasst ## Goldens `buy_payment_info_loaded` ändert sich, `buy_payment_details_default` ist neu — werden via `golden-regenerate.yaml` auf dem Runner erzeugt. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## 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> Co-authored-by: Josh <joshua.krueger@dfx.swiss> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Problem Ein Kunde konnte auf der Registration-Page seine **PLZ nicht eintragen**. ## Ursache Das PLZ-Feld nutzte `keyboardType: TextInputType.number` → reiner Ziffernblock (auf iOS gibt es keine Buchstaben-Tasten). Bei nicht-schweizerischem Wohnsitz sind PLZ aber häufig **alphanumerisch** (🇳🇱 `1011 AB`, 🇬🇧 `EC1A 1BB`, 🇨🇦 `K1A 0B1`, 🇮🇪 `D02 AF30`) — diese ließen sich gar nicht tippen. Der Validator `isSwissPaymentText` und der dazu passende Backend-Decorator `@IsSwissPaymentText` **akzeptieren** Buchstaben/Ziffern/Leerzeichen. Die Tastatur war also strenger als Validator **und** Backend — ein reiner UI-Affordance-Bug. ## Fix `keyboardType` des PLZ-Felds → `TextInputType.text` an beiden Stellen: - `kyc_registration_address_step.dart` (Registration) - `settings_edit_address_page.dart` (Adresse ändern) Plus Regressions-Tests, die den `keyboardType` absichern. Kein Layout-/Golden-Impact. ## CONTRIBUTING-Konformität - **app**: `keyboardType` ist explizit "UI concern" — erlaubt. Keine Business-Logik in der App. - **api**: Format-Autorität bleibt in der API (`Config.formats.swissPaymentText`, gespiegelt in `swiss_payment_text.dart`); keine Capability betroffen, keine API-Änderung nötig.
## Problem Auf der Zahlungsdetails-Seite war der **Verwendungszweck falsch**: er kam (wie der QR) aus dem Quote (`bankUsage`). Der korrekte Verwendungszweck ist die **Aktionariat-Referenz**. ## Reine App-Lösung, abwärtskompatibel Das Confirm-Endpoint liefert **heute schon** `reference` (= die Aktionariat-Referenz = der korrekte Verwendungszweck). Dieser Fix nutzt das **ohne jede API-Änderung**: - **Verwendungszweck** = `remittanceInfo ?? reference` → heute Fallback auf `reference` (korrekt!); sobald die API `remittanceInfo` explizit liefert, wird es bevorzugt (vorwärtskompatibel). - **QR** = nur Confirm-`paymentRequest`, wenn vorhanden → heute **kein** QR (statt des **falschen** Quote-QR mit `bankUsage`!); sobald die API den korrekten QR liefert, erscheint er automatisch. - `RealUnitBuyConfirmDto`: `remittanceInfo` + `paymentRequest` nullable → kein Parse-Fehler gegen die aktuelle API. Damit ist der Kundenbug **sofort** behoben, **ohne Deploy-Kopplung**. Die Bankverbindung (IBAN/BIC/Empfänger/Betrag) kommt weiter aus dem Quote. `buyExecutedReference`-Key entfernt (ungenutzt). ## Tests `flutter analyze` sauber; `flutter test test/screens/buy test/packages/service/dfx` **grün** (564). Backward- **und** Forward-Compat-Pfad getestet (reference-only → Verwendungszweck=reference, kein QR; bzw. remittanceInfo+QR bevorzugt). Golden `buy_payment_details_default` reflektiert den heutigen Ist-Zustand (Verwendungszweck gesetzt, kein QR), regeneriert auf dem Runner. ## Verwandt (Follow-up, NICHT blockierend) DFXswiss/api#3931 designiert `remittanceInfo` explizit + liefert den korrekten QR — sobald gemergt+deployed nutzt die App das automatisch. Bug: DFXswiss/api#3929. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## 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> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fix(buy): Button "Zurück zum Hauptbereich" navigiert zum Dashboard
## Änderung Preis- und Portfolio-Chart auf dem Dashboard initialisierten ihren State mit `TimePeriod.threeMonths`. Der Default ist jetzt **`TimePeriod.all`** — beide Charts zeigen beim Öffnen den **gesamten verfügbaren Zeitraum**. - `lib/screens/dashboard/bloc/price_chart/price_chart_cubit.dart` - `lib/screens/dashboard/bloc/portfolio_chart/portfolio_chart_cubit.dart` ## Tests - `flutter analyze` sauber. - `price_chart_cubit_test` + `portfolio_chart_cubit_test`: **24/24 grün** (alle Tests setzen die Periode explizit via `selectPeriod`; der einzige Default-abhängige Test bleibt grün, da seine Datenpunkte auch unter `all` sichtbar sind). - Veraltete Default-Referenzen in den Test-Kommentaren korrigiert. ## Goldens Das Dashboard-Golden ändert sich (hervorgehobener Perioden-Button wechselt 3M → MAX). Baselines werden auf dem self-hosted Goldens-Runner via `golden-regenerate.yaml` regeneriert und vom Bot auf diesen Branch committet. --------- 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