Skip to content

Release: develop -> main#325

Open
github-actions[bot] wants to merge 302 commits into
mainfrom
develop
Open

Release: develop -> main#325
github-actions[bot] wants to merge 302 commits into
mainfrom
develop

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Automatic Release PR

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

Commits: 1 new commit(s)

Checklist

  • Review all changes
  • Verify CI passes
  • Approve and merge when ready for production

## 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
TaprootFreak and others added 30 commits June 8, 2026 13:15
…) (#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants