Release: develop -> main - #928
Conversation
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
|
EN: DE: DetailsFlow This repo does not grant workflow re-run or |
…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
|
EN: DE: DetailsRetry 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. |
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
|
EN: DE: DetailsFollow-up: #962 — Play Store upload first, continue when assemble is stuck on already-uploaded files. |
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
Automatic Release PR
This PR was automatically created after changes were pushed to develop.
Commits: 2 new commit(s)
Checklist