Skip to content

Release: develop -> main - #928

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

Release: develop -> main#928
github-actions[bot] wants to merge 47 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: 2 new commit(s)

Checklist

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

TaprootFreak and others added 26 commits August 21, 2026 12:40
EN:
The buy flow now shows and quotes exact CHF with rappen instead of
rounding to whole francs.

DE:
Der Kauf zeigt und quotet exakte CHF mit Rappen, statt auf ganze Franken
zu runden.

<details>
<summary>Details</summary>

Typing 10000 CHF for REALU at 1.37 yields 7299 shares. The field now
snaps to 9999.63 (shares times list in Rappen), the quote request keeps
that amount, and payment details render two decimal places.

`chargedFiatAmount` snaps to Rappen only. Sell still sends an integer
share count.

</details>
Promote: staging -> develop
EN:
Adds a live RealUnit share-token geo-filter table to the handbook.
handbook.realunit.app loads GET /v1/country at runtime and does not copy
country rows into the repo.
Until DFXswiss/backend is deployed, the realunit columns stay empty and
the page says so.

DE:
Ergänzt die Live-Tabelle des RealUnit-Aktientoken-Geo-Filters im
Handbook.
handbook.realunit.app lädt GET /v1/country zur Laufzeit und kopiert
keine Länderzeilen ins Repo.
Solange DFXswiss/backend nicht deployed ist, bleiben die
realunit-Spalten leer; die Seite weist darauf hin.

<details>
<summary>Details</summary>

Section `#spec-geo` in `docs/handbook/de/index.html`. Client
`docs/handbook/de/geo-filter.js` tries same-origin `/v1/country` (nginx
proxy in `handbook.nginx.conf`) then `https://api.dfx.swiss/v1/country`.
Screenshot count is unchanged. No Dart/UI change, no goldens.

The geo-filter columns only appear after the merged backend
`CountryDto.realunit` is live on that API.

</details>
EN:
The geo-filter table now shows German and English country names.
CSV, Excel, and PDF downloads export the currently visible rows.
Native-script API foreignName is no longer used.

DE:
Die Geo-Filter-Tabelle zeigt Ländernamen auf Deutsch und Englisch.
CSV, Excel und PDF laden die aktuell angezeigten Zeilen herunter.
Die native foreignName der API wird nicht mehr verwendet.

<details>
<summary>Details</summary>

Names come from `Intl.DisplayNames` (ISO 3166-1) so
Arabic/Japanese/Chinese script in `foreignName` is not shown. Downloads
are generated in the browser from the live `GET /v1/country` payload; no
country rows are copied into the repo.

</details>
…able from state (#934)

EN:
The buy amount field keeps the typed value instead of being overwritten
by the conversion. Quotes still use the Rappen-exact payable. Tests pin
currency-switch and quote wiring so that regression cannot return
silently.

DE:
Das Betragsfeld behält die Eingabe statt sie mit dem Umrechnungsergebnis
zu überschreiben. Quotes laufen weiter über den rappengenauen Betrag.
Tests pinnen Währungswechsel und Quote-Verdrahtung, damit die Regression
nicht still zurückkommt.

<details>
<summary>Details</summary>

Since #926 the conversion result was written back into `fiatText`, and
the BuyView listener synced it into the amount TextField after every
conversion. Editing became impossible: every keystroke or backspace was
overwritten with shares × list price 250ms later (prefill 300 → 299.46;
deleting digits snapped back to the next whole-share multiple and got
stuck). Reported by a customer via RealUnit (only the default amount
worked) and reproduced on-device.

Fix:
- Conversion results land in a new `payableText` only; `fiatText` always
keeps the user's own text (typing in the shares field still converts
into the fiat field, that path is unchanged output).
- All quote requests (initial, listener re-fetch, gate retries in
`PaymentActionButton`) use `quoteAmountText`: the Rappen-exact payable
when a conversion is live, otherwise the typed amount. The #926 goal
(quote and details in Rappen, no whole-franc rounding) stays intact.
- The details page keeps rendering the charge echoed by the quote,
unchanged.
- Tests pin: deleting digits keeps the typed text; currency switch
preserves `fiatText`; `getPaymentInfo` is called with the live payable
rather than the field text.

The exact charge is visible on the confirmation/details page. An
optional display line under the field (`payableText` is already in
state) is a product choice and is not part of this PR.

`flutter analyze` clean on the touched files; `flutter test` of the
three buy test files: 31 passed.

</details>

---------

Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Promote: staging -> develop
…935)

EN:
The buy screen shows the Rappen-exact charge under the amount field
without overwriting what the user typed. Labelling a draft for handbook
flows and later marking it ready no longer starts a second Maestro run.

DE:
Die Kaufseite zeigt den rappengenauen Betrag unter dem Feld, ohne die
Eingabe zu überschreiben. Ein gelabelter Draft startet beim Ready keinen
zweiten Maestro-Lauf.

<details>
<summary>Details</summary>

Buy:
- `payableText` already holds the conversion result. A read-only line
under the amount field renders it when it is non-empty and differs from
the typed amount (e.g. field `300`, line `299.46 CHF`).
- Comparison is numeric, so `300` and `300.00` stay hidden. Punctuation
lives in the widget template, not the ARB value.
- Tests pin visibility of `299.46` and hide when payable is empty,
string-equal, or numerically equal.
- Visual regression: `buy_charged_amount` golden (field `300`, exact
charge `299.46 CHF` under it). Baseline generated on the self-hosted
runner.

CI:
- Observed on #934: `tier3:full` started Maestro; `ready_for_review`
started a second macos-latest run on the same SHA because the labeled
concurrency group is unique.
- `ready_for_review` is removed from Tier 3 `pull_request` types.
`labeled` starts a run only when the event label is `tier3:full`;
`unlabeled` does not start a run. `pull-request.yaml` is unchanged
(draft-skip means ready is the first real Analyze/Visual run).

</details>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
EN:
These tests pin that a EUR buy sends currency EUR and shows the EUR
settlement IBAN from the quote, not a leftover CHF IBAN.
Production buy code is unchanged: it already forwards the selected
currency and renders the IBAN the API returns.
The confirm button is not locked; an EUR quote can still be tapped to
confirm.

DE:
Diese Tests stellen sicher, dass ein EUR-Kauf die Währung EUR sendet und
die EUR-IBAN aus der Quote zeigt, nicht eine übrig gebliebene CHF-IBAN.
Der Produktionscode bleibt unverändert: er leitet die gewählte Währung
bereits weiter und zeigt die IBAN der API.
Der Bestätigen-Button wird nicht gesperrt; eine EUR-Quote lässt sich
weiterhin bestätigen.

<details>
<summary>Details</summary>

Pins the EUR vs CHF buy-quote IBAN contract in existing test files:

- Request body for `PUT /v1/realunit/buy` with `Currency.eur` is
`currency: EUR` and maps the EUR IBAN from the mock response.
- Default/CHF path maps the CHF settlement IBAN.
- `RealUnitBuyPaymentInfoDto.fromJson` parses EUR IBAN + EUR currency.
- `BuyPaymentInfoCubit` success for EUR keeps the EUR IBAN.
- After converter settlement in EUR, `BuyView` refetches the quote with
`Currency.eur`.
- On an EUR `BuyPaymentInfoSuccess`, `BuyConfirmButton` receives the EUR
IBAN (not the CHF leftover).
- Tapping confirm on an EUR quote still calls `confirmPayment`.
- Payment details (after confirm) render the formatted EUR IBAN and
amount-in EUR.

IBANs used as distinct fixtures:

- CHF: `CH2208307000560946309` → `CH22 0830 7000 5609 4630 9`
- EUR: `CH9708307000560946317` → `CH97 0830 7000 5609 4631 7`

No production `lib/` changes.

</details>
…olden (#939)

EN:
A 502 with a plain-text body no longer crashes JSON parsing into a
FormatException on the buy quote screen. The quote retry button remains;
if the API sent no JSON message, only Retry is shown. A visual baseline
pins that retry-only state.

DE:
Ein 502 mit Klartext-Body erscheint auf dem Kauf-Quote-Screen nicht mehr
als FormatException. Der Wiederholen-Button bleibt; ohne JSON-Message
zeigt die UI nur Wiederholen. Eine visuelle Baseline pinnt diesen
Zustand.

<details>
<summary>Details</summary>

Gateway 502 responses with a non-JSON body (e.g. `error code: 502`) were
passed through `jsonDecode`, which threw `FormatException`. That string
was stored on `BuyPaymentInfoFailure.message` and rendered as the info
title.

`ApiException.fromBody` now decodes JSON objects via `fromJson` and
otherwise yields an empty user-facing message with the HTTP status. The
buy payment-info service uses it on error paths. HTTP 502 is mapped like
503 (`priceSourceUnavailable`). The catch-all no longer copies
`Object.toString()` into the UI.

A golden (`buy_price_source_unavailable_empty`) pins the empty-message
retry-only buy screen. The PNG is generated on the self-hosted runner
via `golden-regenerate.yaml`, not locally.

</details>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
EN:
Buying a large CHF amount no longer dies on Retry. The app now shows the
quote cap when the order is above inventory, and it reads 105.000 /
90'000 as one hundred five thousand / ninety thousand instead of
rejecting the input.

DE:
Ein grosser CHF-Betrag stirbt nicht mehr am Retry. Die App zeigt die
Quote-Obergrenze, wenn der Auftrag über dem Bestand liegt, und liest
105.000 / 90'000 als einhundertfünftausend / neunzigtausend statt die
Eingabe zu verwerfen.

<details>
<summary>Details</summary>

Two buy-CTA failures:

1. Quote `AmountTooHigh` / `LimitExceeded` with `maxVolume` used to map
to unknown + Retry, which re-quoted the same over-limit amount.
2. CHF/EUR have at most two decimal places, so `105.000` / `90,000` /
`90'000` are thousands groups. The parser treated them as invalid (or,
on the raw quote API, `105.000` is 105 francs). The field now accepts
the apostrophe and the parser keeps the thousands.

Screenshots of min/max and other quote outcomes now show the typed
amount in the fields, as on the live screen. The max-amount hint still
appears only after an over-inventory quote.

</details>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
#941)

EN:
The app no longer infers a default currency from the phone locale.
Until the user picks one, the display currency is EUR, then the account
currency from GET /v2/user (set from residence on the API: CH/LI → CHF,
otherwise EUR).
A late apply is dropped if the wallet closed or was replaced.

DE:
Die App rät die Default-Währung nicht mehr aus der Telefon-Locale.
Ohne gespeicherte Wahl gilt EUR, danach die Account-Währung von GET
/v2/user (API setzt sie aus dem Wohnsitz: CH/LI → CHF, sonst EUR).
Eine späte Apply wird verworfen, wenn die Wallet geschlossen oder
ersetzt wurde.

<details>
<summary>Details</summary>

Local prefs still win. `ApplyAccountCurrencyEvent` is ignored when a
currency is already stored and is not written to prefs.

Wallet open fetches GET /v2/user and applies that currency. Wallet close
restores the unset default (EUR). Replacing the open wallet instance
clears then re-applies. KYC applies the field after `getUser` only while
that same wallet instance is still open.

Buy reads the settings currency from context. The quote is cleared when
the converter currency changes, and Confirm stays hidden while the
success quote currency lags the picker. Dashboard drops in-flight
price/chart/history for the previous currency.

Sell is unchanged.

</details>
Promote: staging -> develop
)

EN:
The send-process screen waits for its incoming page animation to finish
before starting the transfer or opening the result sheet. A fast prepare
failure no longer paints over the still-sliding confirm page. The
transfer error mapping itself is unchanged.

DE:
Der Sende-Prozess-Screen wartet, bis die eingehende Seitenanimation
fertig ist, bevor Transfer und Ergebnisdialog starten. Ein schneller
Prepare-Fehler übermalt nicht mehr die noch rüberschiebende Prüfseite.
Die Fehlerzuordnung des Transfers selbst bleibt unverändert.

<details>
<summary>Details</summary>

`SendConfirmPage` still pushes `SendProcessPage` with a
`MaterialPageRoute`. `SendProcessCubit.start()` is no longer called from
`BlocProvider.create`. A private route gate starts the cubit once the
real route animation completes.

`ModalRoute.animation` is a `ProxyAnimation` whose parent is
`kAlwaysCompleteAnimation` on the first frame of a push. The gate treats
that placeholder as unresolved so it does not start during the slide.
The result sheet also waits for the same completion.

Widget test: mid-slide the preparing label is shown and the failure
sheet is not; after the animation the failure sheet is shown.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
EN:
Pay, quote load, and sell-bitbox now wait for the incoming route slide
to finish before starting work or opening a result sheet. The
send-process gate is extracted into a shared widget so the same handling
is not duplicated. Fast failures no longer overlay the previous screen
mid-slide.

DE:
Pay, Quote-Laden und Sell-BitBox warten, bis der eingehende Slide fertig
ist, bevor Arbeit startet oder ein Ergebnisdialog aufgeht. Der
Send-Prozess-Gate liegt in einem gemeinsamen Widget, damit dasselbe
Handling nicht dupliziert wird. Schnelle Fehler übermalen nicht mehr die
vorige Seite mitten im Slide.

<details>
<summary>Details</summary>

`RouteAnimationGate` ignores the first-frame `kAlwaysCompleteAnimation`
placeholder. `onSettled` runs from the context below `BlocProvider`.
Result/reconnect sheets still call `waitForIncomingRouteAnimation` as a
second wait.

Widget tests cover the shared gate and the pay-process 300 ms slide.
SellBitbox cubit tests call `start()` explicitly; the constructor no
longer auto-starts.

</details>

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Promote: staging -> develop
EN:
If a software-wallet REALU send already broadcast but the receipt wait
timed out, the app now treats that as success when the HTTP status is
500 and the error names the tx hash. A 400 or 409 whose JSON body
happens to look like a timeout stays a failure. The failure sheet never
shows raw viem or exception text for generic errors. The in-progress
copy no longer asks the user to confirm in a wallet.

DE:
Wenn ein REALU-Send mit Software-Wallet schon broadcastet ist und der
Receipt-Wait ausläuft, zeigt die App Erfolg nur bei HTTP 500 und einem
Fehler, der den tx-Hash nennt. Ein 400 oder 409, dessen JSON-Body wie
ein Timeout aussieht, bleibt ein Fehler. Das Fehler-Sheet zeigt bei
generic keinen viem- oder Exception-Rohtext. Der Fortschrittstext
verlangt keine Wallet-Bestätigung mehr.

<details>
<summary>Details</summary>

- The transfer service maps HTTP 500 plus a receipt-wait timeout phrase
(`timed out while waiting for transaction` and `0x` + 64 hex) to
`TransferReceiptTimeoutException`. The cubit maps only that type to
success.
- Body `statusCode` must not override the HTTP status: HTTP 400/409 with
a timeout-like JSON body stays `ApiException`.
- Generic failures always use localized copy; viem/timeout strings are
never shown.
- `sendSigning`: EN `Sending the transfer` / DE `Überweisung wird
gesendet`.
- The signing-state visual baseline was refreshed from the self-hosted
Visual Regression runner for that copy.
- Retry still reuses the same prepare id.
- Tests on the build host: exception surface, transfer service, send
cubit, send page — 113 passed.

The matching API change is a separate public pull request.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
EN:
Send and Pay no longer stack a second amount or quote page when the
camera keeps seeing the same QR. Both scanner pages re-arm only after
that route pops. A catalog, real-cubit double-capture tests, and a
CONTRIBUTING rule make the same mistake a blocking review finding.

DE:
Senden und Bezahlen stapeln keine zweite Betrags- oder Quote-Seite mehr,
wenn die Kamera denselben QR weiter sieht. Beide Scanner-Seiten rüsten
erst wieder auf, nachdem diese Route geschlossen wurde. Katalog, Tests
mit echtem Cubit und eine CONTRIBUTING-Regel machen denselben Fehler zum
blockierenden Review-Fund.

<details>
<summary>Details</summary>

`QrScannerView` forwards every camera frame. The send recipient page and
the pay scan page used to `Navigator.push` the next step and `reset()`
the cubit in the same listener turn. That dropped the decoded/valid
guard, so the next frame pushed a second copy of the same page.

Both consumers now use `pushThenRearm`: push first, call `reset` only
after the route pops. Invalid-scan snackbars that do not push still
reset immediately.

Tests:

- Catalog `test/helper/scanner_navigation_catalog.dart` plus a discovery
self-test that fails if any `QrScannerView(` under `lib/` is missing
from the catalog.
- Real-cubit widget tests fire two `BarcodeCapture`s and expect the
destination `findsOne`; after pop, a third capture is accepted.
- Existing mock page tests now assert `reset` is not called until pop.

CONTRIBUTING.md has a new **Scanner navigation — CRITICAL** section;
`docs/testing.md` has the matching test contract.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Promote: staging -> develop
EN:
Adds a Tier-1 integration test for the BitBox sell path that starts with
too little ETH.
The cubit posts `/v1/faucet`, polls `/v1/blockchain/balances`, then
signs and broadcasts the unsigned swap and deposit through the DFX API.
The faucet response is held behind a Completer so polling cannot start
until the faucet call returns.
Existing tests that skip the faucet hop are unchanged. No production
code change.

DE:
Ergänzt einen Tier-1-Integrationstest für den BitBox-Sell-Pfad, der mit
zu wenig ETH startet.
Der Cubit sendet POST `/v1/faucet`, pollt `/v1/blockchain/balances` und
signiert dann Swap und Deposit über die DFX-API.
Die Faucet-Antwort liegt hinter einem Completer, damit das Polling erst
nach Rückkehr des Faucet-Calls startet.
Die bestehenden Tests, die den Faucet-Schritt überspringen, bleiben
unverändert. Kein Production-Code.

<details>
<summary>Details</summary>

The previous happy path in `test/integration/sell_bitbox_flow_test.dart`
set `ethBalance` above `requiredGasEth`, so `POST /v1/faucet` never ran.

The new case uses `_info(ethBalance: 0)` and wires the real
`DfxFaucetService` and `DfxBlockchainApiService` (plus the already-real
sell service) to the same `MockClient`. Sequence:

1. `POST /v1/faucet` is entered; the 200 is held on a Completer
2. a 5s elapse must not call `/v1/blockchain/balances` (timer not
installed yet)
3. Completer completes; cubit is `WaitingForEth`
4. after the 5s poll timer, `POST /v1/blockchain/balances` with
`blockchain=Ethereum` and ETH asset id 111
5. unsigned swap + deposit, two FakeBitbox signs, two broadcasts, one
confirm
6. `SellBitboxSuccess`

Unsigned/broadcast/confirm before the faucet POST fail the test. No
BitBox hardware and no live node. Local checks on the build host:
analyze clean, format unchanged, `flutter test
test/integration/sell_bitbox_flow_test.dart` +6 passed.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
EN:
This PR reproduces the customer chart bug as goldens, without a product
fix.
MAX shows the old holdings curve, then a drop to zero. 1W, 1M, 3M and 1J
show the empty plot.

DE:
Dieser PR bildet den Kunden-Chart-Bug als Goldens ab, ohne Produkt-Fix.
MAX zeigt die alte Bestandskurve, dann den Abfall auf 0. 1W, 1M, 3M und
1J zeigen die leere Fläche.

<details>
<summary>Details</summary>

Fixtures: non-zero points older than one year (MAX curve), then zeros
through now (1W–1J collapse with `minY == maxY == 0`). Same sequence the
customer reported. PNGs from the self-hosted visual-regression runner.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
EN:
The portfolio development chart disappeared for time windows where every
holding is zero.
The cubit now keeps a Y-range from 0 so the line stays drawn, and holds
the last value before the window so the stroke spans the selected
period.
A dashboard golden records the all-zero MAX view; the customer-report
1W–1J goldens show that full-width zero line on a 0…10 axis.
Unit tests cover an all-zero series, a one-week window of zeros, and
carrying a pre-window sample to minX.

DE:
Die Bestandsentwicklung verschwand in Zeitfenstern, in denen jeder Punkt
0 ist.
Der Cubit hält jetzt eine Y-Spanne ab 0, damit die Linie gezeichnet
bleibt, und übernimmt den letzten Wert vor dem Fenster, damit der Strich
die gewählte Periode ausfüllt.
Ein Dashboard-Golden dokumentiert die Null-MAX-Ansicht; die
Kunden-Goldens 1W–1J zeigen diese volle Null-Linie auf einer Achse 0…10.
Unit-Tests decken eine Null-Serie, ein 1W-Null-Fenster und das
Nachziehen eines Werts vor minX ab.

<details>
<summary>Details</summary>

When every visible point is 0, `average * 0.05` was 0, so `minY == maxY
== 0` and `LineChart` drew no curve. The cubit now falls back to a 5.0
currency-unit floor so the chart has a Y-range. Holdings cannot go
negative: the bottom line is always clamped to 0 (scale 0…10 for an
all-zero series), not centered through negative labels.

A clipped period (1W/1M/3M/1J) previously started the stroke at the
first interior sample. If that sample sat months into the window, the
line began mid-chart. The last sample before `minX` is now held at the
window edge so the stroke spans the selected period. An empty window
(every sample older than the period) stays empty.

`dashboard_portfolio_chart_zero.png` is the all-zero MAX view. After
#953, the mixed-history 1W/1M/3M/1J goldens pin the full-width zero line
on the 0-axis.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
EN:
Five synthetic test customers document different buying patterns on the
holdings chart.
Each history has at least ten trades inside one year; the diagrams are
golden baselines, not live accounts.
Handbook spec 80 lists every trade and maps six screenshots (MAX for all
five, plus 1J after a sell-to-zero).
The screenshot count guard moves from 278 to 284.

DE:
Fünf synthetische Testkunden dokumentieren unterschiedliches
Kaufverhalten auf der Bestandsentwicklung.
Jede Historie hat mindestens zehn Trades in einem Jahr; die Diagramme
sind Golden-Baselines, keine Live-Konten.
Handbook-Spec 80 listet alle Trades und sechs Screenshots (MAX für alle
fünf, plus 1J nach Verkauf auf 0).
Der Screenshot-Count-Guard steigt von 278 auf 284.

<details>
<summary>Details</summary>

Personas: monthly DCA (12×80), lump then top-ups, eight buys then four
sells to zero, interleaved mix, rising scale. Fixtures are
local-midnight offsets so MAX and 1J stay stable. No addresses or
personal data.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
EN:
Handbook PRD deploys have been failing because the mail-preview
generator now writes 28 HTML files, not 25.
This updates the exact-count guard so a push to staging can ship the
handbook again, including spec 80.
No product UI change.

DE:
Die Handbook-PRD-Deploys schlagen fehl, weil der Mail-Preview-Generator
jetzt 28 HTML-Dateien schreibt, nicht 25.
Dieser Guard-Stand lässt den Staging-Push wieder nach PRD, inklusive
Spec 80.
Keine Produkt-UI-Änderung.

<details>
<summary>Details</summary>

Count is 24 standalone `add()` mails + 3 `pendingTypes` expansions + 1
index = 28. The loop `add()` template is not counted in the 24.
Confirmed against the failed deploy log on staging (got 28).

The three extra previews come from DFXswiss/backend#5221: `black-squad`,
`kyc-payment-data`, `recommendation-confirmation`. The handbook page
`#spec-mails` only links to the generated `/mails/` index, so no extra
cards.

This unblocks #952 (`Promote: staging -> develop`), whose
only red check is `deploy-prd / Build and deploy to PRD`.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Promote: staging -> develop
…955)

EN:
Send is gone from the dashboard. The settings wallet-address screen
shows the QR, the stock-token hint, and Send. Tapping Send first shows
that transfers only reach registered shareholders and update the share
register. Tapping Pay first shows that payment is a two-step
REALU-to-ZCHF swap usable at OpenCryptoPay locations, because REALU is a
security.

DE:
Send ist vom Dashboard weg. Die Wallet-Adresse in den Einstellungen
zeigt den QR, den Hinweis zu den Aktientoken und Senden. Vor dem Senden
erscheint der Hinweis: nur an registrierte Aktionäre, das Aktienbuch
wird aktualisiert. Vor dem Bezahlen: Zwei-Schritt-Tausch REALU zu ZCHF
an OpenCryptoPay-Standorten, weil REALU ein Wertpapier ist.

<details>
<summary>Details</summary>

Dashboard keeps Buy, Sell, and insider-gated Pay. `/send` and `/pay`
land on info screens; Continue opens the existing recipient scanner or
OpenCryptoPay scanner. The QR address row shrink-wraps so the cluster
stays centered. Shareholder enforcement for the transfer itself lives in
the companion backend change.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Promote: staging -> develop
@TaprootFreakAI

Copy link
Copy Markdown
Collaborator

EN:
Maestro handbook flows was cancelled after the XCUITest driver returned HTTP 500 and the JVM hung until the 60-minute timeout; the required checks on this SHA are already green.

DE:
Maestro handbook flows wurde abgebrochen, nachdem der XCUITest-Treiber HTTP 500 lieferte und die JVM bis zum 60-Minuten-Timeout hing; die Pflicht-Checks auf diesem SHA sind bereits grün.

Details

Flow 01-welcome died with UnknownFailure on http://127.0.0.1:7001/deviceInfo (HTTP 500), then sat until GitHub cancelled the job. That check is not in the required-status set (Analyze & Test, Visual Regression, Coverage Floor Gate).

This repo does not grant workflow re-run or workflow_dispatch to this account. The retry/timeout fix is in #958 (targets staging).

TaprootFreakAI and others added 2 commits August 29, 2026 13:26
…es (#958)

EN:
Maestro retries XCUITest driver HTTP 500 hangs instead of sitting until
the job timeout. Handbook persona goldens keep trade dates on 2026-08-28
so Visual Regression does not go red at midnight.

DE:
Maestro retried bei XCUITest-Driver-HTTP-500, statt bis zum Job-Timeout
zu warten. Die Handbook-Persona-Goldens halten die Trade-Daten auf dem
28.8.2026, damit Visual Regression nicht um Mitternacht rot wird.

<details>
<summary>Details</summary>

The cancelled Maestro check on the release PR is not a required status
check. Analyze & Test, Visual Regression, and Coverage Floor Gate
already passed on that SHA.

`is_driver_hang_or_death` matches `deviceInfo failed, code: 500`. Each
`maestro test` is killed after `MAESTRO_ATTEMPT_TIMEOUT_SEC` (default
480) and retried. Handbook persona `_today()` is a fixed date, not
`DateTime.now()`.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Promote: staging -> develop
@TaprootFreakAI

Copy link
Copy Markdown
Collaborator

EN:
android-deploy failed on Sentry native-symbol assemble (libapp.so / app.so: not all chunks available) after the CLI said the files were already on the server. iOS deploy succeeded. Play Store never got this AAB because symbol upload runs before upload_to_play_store.

DE:
android-deploy ist am Sentry-Native-Symbol-Assemble gescheitert (libapp.so / app.so: nicht alle Chunks da), nachdem die CLI die Dateien schon auf dem Server gemeldet hatte. iOS-Deploy ist durch. Der Play Store hat dieses AAB nicht bekommen, weil der Symbol-Upload vor upload_to_play_store läuft.

Details

Retry helper: #960 (targets staging). A re-run of the failed Release job still uses the tagged Fastfile without the retry; the change applies on the next develop tag after that PR merges. This account cannot re-run GitHub Actions jobs.

TaprootFreakAI and others added 2 commits August 29, 2026 17:59
EN:
Android deploy on the v1.2.20 tag failed while uploading native symbols:
Sentry reported the files were already on the server, then failed
assembling libapp.so chunks. This retries that upload three times so a
flake does not block the Play Store upload.

DE:
Der Android-Deploy auf Tag v1.2.20 ist beim Native-Symbol-Upload
gescheitert: Sentry meldete die Dateien schon auf dem Server und
scheiterte dann am Zusammensetzen der libapp.so-Chunks. Dieser Stand
retried den Upload dreimal, damit ein Flake den Play-Store-Upload nicht
blockiert.

<details>
<summary>Details</summary>

iOS deploy on the same Release run succeeded. Analyze / Visual on the
release SHA were still running at the time of this PR. The Play Store
lane never reached `upload_to_play_store` because symbol upload runs
first.

Re-running the failed Release job still uses the tagged Fastfile without
this retry. This change takes effect on the next develop tag after
merge.

</details>

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Promote: staging -> develop
@TaprootFreakAI

Copy link
Copy Markdown
Collaborator

EN:
v1.2.21 android-deploy failed again: Sentry retried 3/3, files already on the server, app.so assemble still stuck. iOS succeeded. Play Store still did not get this AAB.

DE:
v1.2.21 android-deploy ist wieder gescheitert: Sentry 3/3 retried, Dateien schon auf dem Server, app.so-Assemble weiter hängend. iOS ist durch. Der Play Store hat dieses AAB weiter nicht.

Details

Follow-up: #962 — Play Store upload first, continue when assemble is stuck on already-uploaded files.

TaprootFreakAI and others added 17 commits August 30, 2026 06:00
EN:
v1.2.21 android-deploy retried Sentry symbol upload three times and
still failed: the CLI said the files were already on the server, then
assemble of app.so chunks failed. This uploads the Play Store AAB first
and continues when that stuck assemble is the only error.

DE:
v1.2.21 android-deploy hat den Sentry-Symbol-Upload dreimal retried und
ist trotzdem gescheitert: die CLI meldete die Dateien schon auf dem
Server, danach scheiterte das Assemble der app.so-Chunks. Dieser Stand
lädt das AAB zuerst in den Play Store und macht weiter, wenn nur dieses
hängende Assemble fehlschlägt.

<details>
<summary>Details</summary>

Missing symbol files still fail the lane immediately. Other Sentry
errors still retry three times and then fail. iOS gets the same
continue-on-stuck-assemble path.

A re-run of the v1.2.21 Release job still uses the tagged Fastfile
without this change. It applies on the next develop tag after merge.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Promote: staging -> develop
EN:
Pins the current history titles: inbound REALU from an external wallet
is labelled Kauf. A follow-up PR will change the same screenshot to
Empfangen.

DE:
Hält die heutigen History-Titel fest: eingehende REALU von einer fremden
Wallet heissen Kauf. Ein Folge-PR ändert denselben Screenshot zu
Empfangen.

<details>
<summary>Details</summary>

No production-code change. One golden
(`transaction_history_transfer_labels`) with four rows (100 in, 10 in,
10 out, 20 out). PNGs come from `golden-regenerate.yaml` on the
self-hosted runner — not generated locally.

The parked fix is #965 / `feat/history-transfer-labels`.
After this merge it will be rebased so GitHub shows the pixel
before/after on this file.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
#969)

EN:
Labels inbound REALU from an external wallet as Empfangen instead of
Kauf, using the API category. The history screenshot diffs against the
baseline from #967.

DE:
Beschriftet eingehende REALU von einer fremden Wallet als Empfangen
statt Kauf, anhand der API-Kategorie. Der History-Screenshot diffed
gegen die Baseline aus #967.

<details>
<summary>Details</summary>

Same four rows as #967. The shared golden
`transaction_history_transfer_labels` changes +10 from Kauf to Empfangen
and −10 from Verkauf to Gesendet. Brokerbot rows stay Kauf/Verkauf.

Follows closed #965 (fork head could not be force-pushed after the
rebase).

</details>

---------

Co-authored-by: Blume1977 <jana.ruettimann@dfx.swiss>
Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary / Zusammenfassung

**EN.** RealUnit referral programme and promo-code client (Offerte
24.08.2026 + mail Dani 17:42). The DFX API is the decision authority;
this app is the rendering layer for eligibility, terms, invites,
overview, frozen-CHF prizes, the KYC invite/promo step, and App Links.
This branch supersedes #942 with clean, signed, tool-neutral commits and
carries the review finalisation on top of the feature. It was hardened
through a cross-vendor double-review (Claude Opus 5 + Codex) to zero
findings.

**DE.** RealUnit-Empfehlungsprogramm und Promo-Code in der App (Offerte
24.08.2026 + Mail Dani 17:42). Die DFX-API entscheidet; die App rendert
Kachel, Teilnahmebedingungen, Einladungen, Übersicht, Prämien mit
eingefrorenem Frankenwert, KYC-Schritt Invite/Promo und App Links.
Dieser Branch ersetzt #942 mit sauberen, signierten, werkzeugneutralen
Commits und enthält die Review-Finalisierung zusätzlich zum Feature. Er
wurde per Cross-Vendor-Doppelreview (Claude Opus 5 + Codex) auf null
Findings gehärtet.

<details>
<summary>Scope, review finalisation, measurements</summary>

### Scope

- Dashboard and settings tiles gated on `GET
/v1/realunit/referral/summary` `eligible` (KYC-verified, ≥ 70 REALU, not
an employee). Subtitle **Erhalte 20 REALU pro Weiterempfehlung**.
- Teilnahmebedingungen from authenticated `GET
/v1/realunit/referral/terms` (`version`, `markdown`, `markdownEn`), with
bundled assets only as an outage fallback; create-invite only after the
acceptance checkbox. Acceptance posts the exact rendered version so a
newer server version reopens the gate, and the posted version always
matches the text actually shown.
- Server-generated share text, copy and send; invite URL always
`https://realunit.app/invite/{code}`.
- Overview: **Open / Credited** only (TB Ziff. 7); defensive client
parsing drops Deleted/expired rows. Total tile label **Aktienkurs**.
History uses CHF frozen at credit (TB Ziff. 6).
- KYC registration step for invite and promo (promo Aktionstext in its
own dialog), on the sticky `ScrollableActionsLayout` inside a `SafeArea`
so next/skip clear system insets. No stacking with an invite prize (API
rejects).
- Universal Links / App Links; Play Install Referrer on Android; iOS
re-tap / paste.
- Prize rows: whole REALU, date, frozen CHF. History watch includes
`referralPayout` (type 5).
- DE/EN, handbook `#spec-referral`. Referral/promo goldens regenerated
via `golden-regenerate.yaml`.

### Review finalisation on top of the feature (this replaces #942)

- **Correctness:** symmetric DE/EN precedence in
`campaignTextForLocale`; a generation guard in the eligibility reload;
terms acceptance reset on a language reload, adopting the API terms
version only together with the text it belongs to; the overview
Equatable state compares every rendered per-invite field plus the
share-price label.
- **Conformity:** the KYC step uses the sticky `ScrollableActionsLayout`
in a `SafeArea` and is covered by a responsive-matrix test that asserts
both sticky CTAs; the pure `format_frozen_chf` helper moved into
`packages/io`; the frozen-CHF label uses the text theme; the unused
payout merger and the orphaned `getPayouts` service method were removed;
the referral surfaces are analyzer-clean.
- **Second review round:** a further cross-vendor double-review (Claude
Opus 5 logic + Codex conformity) drove the finalisation to zero findings
again. Fixed: the eligibility cubit now guards every post-await `emit`
with `isClosed` (no emit-after-close when a cubit is closed mid-fetch);
KYC registration hands the deeplink stash to the referral field only
while the referral step is the active step, preserving the
do-not-refill-after-skip contract while the field keeps
stash-over-clipboard precedence; `frozen_chf_label` moved into
`lib/widgets/` (only non-referral importers) with imports re-sorted per
CONTRIBUTING.md; the test-only pending-code sync helpers marked
`@visibleForTesting`; `shareReferralInvite` annotated for
integration-test exclusion. New regression test: the eligibility cubit
does not emit after being closed mid-flight.
- **Deferred with rationale (not defects):** the lenient `referralJson*`
DTO helpers are kept deliberately (locale-tolerant CHF/number parsing;
strict `as` casts would throw on the API's locale-formatted values); a
few raw `TextStyle(color:)` sites are left as-is (a golden-affecting
style refinement tracked as follow-up, not a correctness issue); the
`NEEDS_TERMS` retry-only screen is escapable via back and only reachable
through an unusual backend state race (reviewer: not a blocker).

### Measured (self-hosted / native)

| Check | Result |
| --- | --- |
| `flutter test --exclude-tags golden` | **5951 passed** |
| `flutter analyze` | **0 issues** (repo's enabled lints applied across
the referral feature) |
| Referral + routing suites | **545 passed** |
| Round-2 re-verify on the Flutter host (`flutter analyze` +
referral/kyc/routing suites at `fef34be`) | **0 issues, 635 passed** |
| `golden-regenerate.yaml` on RealUnitCH/app | two successful runs
updating the KYC-step + frozen-CHF baselines
([33922437059](https://github.com/RealUnitCH/app/actions/runs/33922437059)
→ `fddb4ec`,
[33925355107](https://github.com/RealUnitCH/app/actions/runs/33925355107)
→ `69db0ea`) |

### Post-merge / operator actions

- **Backend rollout:**
[DFXswiss/backend#5415](DFXswiss/backend#5415)
implements and tests the matching routes; merge/deploy remains
human-owned.
- Human: merge
[RealUnitCH/web#30](RealUnitCH/web#30) so
production AASA / assetlinks / `/invite/*` are HTTP 200.
- Human: `PRIZE_WALLET_KEY` for settled 20 REALU transfers.

Related:
[RealUnitCH/intern#3](RealUnitCH/intern#3) (do
not auto-close).

</details>

---------

Co-authored-by: Jonny Luca <320529100+JonnyLuca@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Promote: staging -> develop
EN:
The Aktientoken geo-filter table now shows only ja/nein for every
decision column. Residence Allowed maps to ja, everything else to nein.
Nationality Blocked maps to nein, everything else to ja. Filters, CSV,
Excel and PDF use the same values.

DE:
Die Aktientoken-Geo-Filter-Tabelle zeigt in jeder Entscheidungsspalte
nur noch ja/nein. Wohnsitz Allowed wird zu ja, alles andere zu nein.
Nationalitaet Blocked wird zu nein, alles andere zu ja. Filter, CSV,
Excel und PDF nutzen dieselben Werte.

<details>
<summary>Details</summary>

Keine Enum-Labels mehr in Tabelle oder Dropdowns. Quelle bleibt live
`GET /v1/country`.

</details>

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
EN:
The additional tax-residence picker hides countries with taxEnable
false.
Nationality still lists every country; residence still uses kycAllowed.
The country fixture now includes production realunit.taxEnable so
goldens match the filter.
S5 uses Italy instead of the United States.

DE:
Der zusätzliche Steuerwohnsitz-Wähler blendet Länder mit taxEnable false
aus.
Nationalität zeigt weiter alle Länder; Wohnsitz weiter kycAllowed.
Die Länder-Fixture enthält jetzt production realunit.taxEnable, damit
Goldens den Filter zeigen.
S5 nimmt Italien statt der USA.

<details>
<summary>Details</summary>

`CountryFieldPurpose.tax` allows `taxEnable != false`. Unlocked tax-step
rows use that purpose. Fixture refresh from GET /v1/country. Regenerated
`kyc_registration_tax_step_dropdown_open` and S5 DE+FR+IT goldens.
Handbook mapping 61j/61k updated.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…eilnahmebedingungen (#974)

EN:
Accepting the referral terms now uses PUT, matching the API, so
TestFlight users can get past the Teilnahmebedingungen.
Bundled fallback terms and the version string are the binding 26.08.2026
text (first buy of at least 200 REALU).
Deploy the matching API first; otherwise accept of version 2026-08-26 is
rejected with 409.

DE:
Das Akzeptieren der Teilnahmebedingungen nutzt jetzt PUT, wie die API es
erwartet, damit TestFlight-Nutzer die Seite passieren können.
Die gebündelten Fallback-Bedingungen und die Versionszeichenkette sind
der verbindliche Text vom 26.08.2026 (Erstkauf mindestens 200 REALU).
Zuerst die passende API deployen; sonst wird die Annahme der Version
2026-08-26 mit 409 abgelehnt.

<details>
<summary>Details</summary>

Problem: the app sent POST /v1/realunit/referral/terms/accept while the
API defines PUT. The 404 Cannot POST was mapped to the unavailable copy.

Changes:
- `acceptTerms` uses `authenticatedPut`; service test pins
`request.method == PUT`.
- Bundled `assets/legal/referral_terms_{de,en}.md` section 4 and Stand
line updated to 26.08.2026.
- `ReferralTermsDto.bundledVersion` = `2026-08-26`, matching
DFXswiss/backend#5481.
- Docs and handbook section R: first-buy minimum (N REALU, default 200,
set by the API). Text only; goldens untouched (they render an inline
markdown stub).

Dependencies: deploy DFXswiss/backend#5481 first.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
EN:
Adds a PNG wordmark for RealUnit HTML mails. Gmail and Outlook do not
render the WordPress SVG. Backend mail templates will point at this
file.

DE:
PNG-Wortmarke fuer RealUnit-HTML-Mails. Gmail und Outlook zeigen das
WordPress-SVG nicht. Die Backend-Mail-Templates verweisen auf diese
Datei.

<details>
<summary>Details</summary>

Converted from the existing RealUnit SVG wordmark. Public path so mail
clients can fetch it.

</details>

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
…andbook (#976)

EN:
The RealUnit handbook chapter Empfehlungen & Promo-Code now states the
referral prize is only due when the invitee's first completed REALU buy
is at least 200, checked on the server. Promo codes keep that floor on
their own min-buy field.

DE:
Das Handbook-Kapitel Empfehlungen & Promo-Code sagt jetzt, dass die
Empfehlungsprämie erst fällig ist, wenn der erste abgeschlossene
REALU-Kauf der eingeladenen Person mindestens 200 REALU beträgt,
serverseitig geprüft. Promo-Codes behalten dieselbe Schwelle auf ihrem
eigenen Mindestkauf-Feld.

<details>
<summary>Details</summary>

The intro already mentioned the 200 REALU first-buy floor for the
inviter prize (from #974). Dani compared the promo-code sentence and
still saw the threshold only there. This adds the same floor next to
that promo sentence and in `docs/referral-programme.md` (the contract
the chapter links).

Base is `staging` (handbook.realunit.app deploys from staging).

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
EN:
Handbook PRD deploys fail because the mail-preview generator now writes
26 HTML files, not 28.
This updates the exact-count guard so a push to staging can ship the
handbook again, including the 200 REALU referral floor from #976.
No product UI change.

DE:
Die Handbook-PRD-Deploys schlagen fehl, weil der Mail-Preview-Generator
jetzt 26 HTML-Dateien schreibt, nicht 28.
Dieser Guard-Stand lässt den Staging-Push wieder nach PRD, inklusive der
200-REALU-Schwelle aus #976.
Keine Produkt-UI-Änderung.

<details>
<summary>Details</summary>

Count is 22 standalone `add()` mails + 3 `pendingTypes` expansions + 1
index = 26.
Confirmed against the failed deploy log on staging (got 26).

The two missing previews are DFX-only (`black-squad`,
`kyc-payment-data`). The handbook page `#spec-mails` only links to the
generated `/mails/` index, so no extra cards.

This unblocks the handbook deploy after #976.

</details>

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
EN:
Handbook PRD deploys fail because RealUnitCH/web now commits 146
Playwright baselines, not 36.
This updates the exact-count guard and adds #spec-web cards for the
/invite/ and /promo/ landings so a push to staging can ship the handbook
again, including the 200 REALU referral floor.
No product UI change.

DE:
Die Handbook-PRD-Deploys schlagen fehl, weil RealUnitCH/web jetzt 146
Playwright-Baselines committet, nicht 36.
Dieser Guard-Stand plus die #spec-web-Karten für /invite/ und /promo/
lässt den Staging-Push wieder nach PRD, inklusive der
200-REALU-Schwelle.
Keine Produkt-UI-Änderung.

<details>
<summary>Details</summary>

110 new PNGs are the invite and promo landings (ok, fallback, loading,
invalid, missing-code, spent, unavailable, noscript) across desktop,
tablet, and mobile.
The previous 36 cards (home, confirm, account-merge, 404) stay as they
were.
Count is 146, confirmed against RealUnitCH/web `develop`
`tests/__screenshots__/`.

Same pattern as #920 (account-merge 22 → 36).

</details>

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
…docs (#975)

EN:
Referral terms are in-app Markdown under assets/legal, same as privacy
and terms of use.
The wallet no longer renders GET /terms markdown.
Accept still sends version 2026-08-26. Companion API change stops
shipping the full text.

DE:
Die Referral-Teilnahmebedingungen liegen als In-App-Markdown unter
assets/legal, wie Datenschutz und Nutzungsbedingungen.
Die Wallet rendert den GET-/terms-Text nicht mehr.
Accept sendet weiter Version 2026-08-26. Die begleitende API-Änderung
liefert den Volltext nicht mehr.

<details>
<summary>Details</summary>

The previous approach treated the API as the author of the terms. That
did not match the RealUnit legal setup (wallet Markdown, API version
stamp). This PR restores `assets/legal/referral_terms_*.md` (26 August
2026) and loads them like LegalDocumentPage. Handbook legal downloads
include referral_terms again.

A companion API change empties GET markdown while keeping the version
field and PUT accept.

</details>

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
… in Open Sans (#981)

EN:
AppBar titles and `TextSubstringHighlighting` descriptions now use Open
Sans instead of the test/system font. Goldens were regenerated on the
self-hosted runner (the only PNG that still drifted was the QR-SVG
payment tab). Referral terms stubs match the bundled 26.08.2026 stand.

DE:
AppBar-Titel und `TextSubstringHighlighting`-Beschreibungen nutzen jetzt
Open Sans statt Test-/Systemschrift. Die Goldens kamen vom self-hosted
Runner (einzige Nachdifferenz: QR-SVG-Payment-Tab). Die
Referral-Terms-Stubs entsprechen dem gebündelten Stand 26.08.2026.

<details>
<summary>Details</summary>

Replaces the fork PR of the same commits so first-party CI can run (fork
workflows stayed `action_required`).

- `lib/styles/themes.dart`: `appBarTheme.titleTextStyle` sets
`fontFamily: RealUnitTextStyle.fontFamily`.
- `lib/widgets/text_substring_highlighting.dart`:
`DefaultTextStyle.of(context).style.merge(style)` because `RichText`
does not inherit.
- Goldens via `golden-regenerate.yaml`; empty signed commit after the
bot push.
- Merge this before the handbook-prose PR.

</details>

---------

Co-authored-by: Blume1977 <jana.ruettimann@dfx.swiss>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
…goldens and web baselines (#982)

EN:
Handbook consistency pass: every card was checked against the PNG,
German UI strings, page code and neighbouring sections. Follow-up
commits remove private backend paths from public docs, match the PIN
lockout caption to the 1/2/5/10-minute cascade, and name screens plus
widgets golden trees together.

DE:
Handbook-Konsistenz: jede Karte gegen PNG, deutsche UI-Texte, Page-Code
und Nachbarsektionen. Nachzüge entfernen interne Backend-Pfade aus
öffentlichen Docs, bringen die PIN-Sperre auf 1/2/5/10 Minuten und
nennen screens- und widgets-Golden-Bäume gemeinsam.

<details>
<summary>Details</summary>

Replaces the fork PR of the same commits so first-party CI can run. No
product UI change. Merge the font/golden PR first.

</details>

---------

Co-authored-by: Blume1977 <jana.ruettimann@dfx.swiss>
Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@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