Release: develop -> main - #921
Open
github-actions[bot] wants to merge 10 commits into
Open
Conversation
…897) Behebt #896: Die Bildbeschreibung von Handbuch-Block `251-support-create-ticket-page-filled` sagt „mit ausgewähltem Anliegen-Typ und **eingegebener Nachricht**", die Baseline zeigte aber den grauen Platzhalter **Nachricht eingeben**. Dieser PR ändert den Test, der das Bild erzeugt, und die Baseline — der veröffentlichte Handbuch-Screenshot 251 ändert sich damit mit. **Not symptom-driven:** Keine Störung in Produktion. Auslöser ist ein Review-Befund auf #893, dort bewusst nicht mitbehoben. Der Widerspruch ist am Artefakt geprüft, nicht aus dem Issue übernommen: Baseline angesehen (Platzhalter statt Text, Senden aktiv), Ursache im Code belegt — das `TextField` in `support_create_ticket_page.dart` ist unkontrolliert und treibt den Cubit nur über `onChanged`, der gemockte `state.message` erreicht den Render-Baum nie. **Scale:** Ein Handbuch-Block, eine Baseline, ein Golden-Test. Die Fläche ist die Beschreibung selbst — für Nutzer mit Screenreader **ist** sie das Bild, und sie zählte ein Formularfeld auf, das sichtbar leer war. Genau diese Klasse wurde in #893 für 250/251/252/258/259 nachgezogen. **Smaller fix considered:** Nur den Beschreibungstext umschreiben („Feld noch leer, Senden trotzdem aktiv") — 2 Zeilen, verworfen: dann zeigt das Handbuch dauerhaft einen Zustand, den es in Produktion nicht gibt (aktiver Senden-Knopf bei sichtbar leerem Pflichtfeld). Der gewählte Weg macht Bild und Beschreibung beide wahr; das Handbuch-HTML bleibt unverändert. ## Was drin ist `pumpBeforeTest` tippt denselben Text, den der State trägt, räumt den Fokus ab und lässt die `InputDecorator`-Rückblende `focusedBorder` → `enabledBorder` ablaufen. Das Bild zeigt danach, was ein Nutzer nach dem Tippen sieht: Text im Feld, kein Cursor, grauer Rahmen wie im Nachbarbild 250. Zwei Assertions halten das fest — beide **an einer plausibel-falschen Variante rot geprüft**, nicht nur an ihrer Abwesenheit: | Mutation | Ergebnis | |---|---| | anderer Text getippt als im State | rot — `Found 0 widgets with text "Ich habe eine Frage zu meinem Konto."` | | `unfocus`-Zeile entfernt | rot — `Expected: false / Actual: <true>` | Die erste sichert den Kern des Fixes: ein still wirkungsloses `enterText` würde sonst wieder ein leeres Feld einfrieren — genau der Fehlermodus, um den es hier geht. Die zweite sichert, dass weder Cursor noch Fokusrahmen ins Bild geraten. Ohne das `pumpAndSettle` friert der Rahmen auf der Fokusfarbe ein: Pixel `(20, 310)`, das linke Rahmenpixel des Felds, liest dann `(25, 136, 198)` statt `(226, 232, 240)`. Das war ein Befund am erzeugten Bild, nicht am Code. Der Umfang ist bewusst auf diesen Zuschnitt begrenzt. Aus Review-Runden waren zwischenzeitlich weitere Assertions (Senden-Knopf, Chip-Auswahl, ein Scheduler-Wächter), ein `precacheImages`-Aufruf und die Vereinheitlichung der Nachrichten-Konstante über alle Goldens der Datei dazugekommen — alles zurückgenommen: die fünf Nachbar-Goldens derselben Datei tragen kein solches Gerüst, und das Issue verlangt ausdrücklich einen Eingriff, der *nur diese eine Baseline* trifft. Dass die Goldens nach dem Rückschnitt weiter 6/6 grün sind, belegt zugleich, dass keine dieser Zutaten je das Bild beeinflusst hat. ## Baseline Regeneriert auf dem self-hosted Runner ([Lauf 31171522710](https://github.com/RealUnitCH/app/actions/runs/31171522710)), nicht auf meinem Mac — damit sie unter derselben Toolchain rendert, die sie auf PRs validiert. Der Bot-Commit war unsigniert; seine Bytes liegen hier als eigener **signierter** Commit, SHA256 gegen die Runner-Ausgabe geprüft (`7c15a7a1f76b83fe…`), der Bot-Commit wurde dafür per Force-Push ersetzt. Der Branch hat nur einen Autor und keine Reviews, es ging dabei nichts verloren. Nebenbefund: Runner und lokaler Mac haben das Bild **byte-identisch** erzeugt — derselbe SHA256. Für diese Fläche ist die Toolchain also deckungsgleich. Der Pixel-Diff der Baseline ist auf die Textzeile begrenzt: Bounding-Box x37–337 / y267–283, 2122 Pixel = 0,64 %. Rahmen und Senden-Knopf sind byte-gleich zum vorherigen Bild. Der Screenshot enthält keinen QR-/Barcode (`zbarimg`, Exit 4) und keine Zugangsdaten — nur Formularfelder und den Satz „Ich habe eine Frage zu meinem Konto.". ## Verifikation `flutter analyze` ohne Befund. `flutter test` auf der Golden-Datei gegen die neue Baseline: **6 von 6 grün**. Vor der Regenerierung war es 5 grün / 1 rot mit `Pixel test failed, 0.64%, 2122px` — ein Bildvergleich, kein Assertion-Fehler; die anderen fünf Baselines stimmten dabei pixelgenau, der Vergleich war also aussagekräftig. Drei unabhängige Prüfpässe sind gelaufen (Konformität gegen `CONTRIBUTING.md`, Logik/Korrektheit mit eigenen Mutationen, Katalog-Pass gegen die Review-Historie dieses Repos); ihre Funde sind eingearbeitet, soweit sie im Zuschnitt dieses Issues liegen. ## Bewusst nicht angefasst - **Golden 252** (`submitting`): zeigt weiterhin das leere Feld — und trägt damit **dieselbe Unerreichbarkeit** wie 251 vor diesem PR: `canSubmit` verlangt `message.trim().isNotEmpty` (`support_create_ticket_state.dart:46-50`), ein Absendevorgang mit sichtbar leerem Pflichtfeld kommt in Produktion nicht vor. Dass die Beschreibung dort keinen sichtbaren Text behauptet, macht das Bild wörtlich nicht falsch, deckt die Unerreichbarkeit aber nicht ab — die frühere Begründung an dieser Stelle war zu schwach. Der Grund fürs Draußenlassen ist der Zuschnitt, nicht die Harmlosigkeit: Issue #896 führt 252 ausdrücklich als „nicht betroffen", und technisch ist das `TextField` dort über `enabled: !state.isSubmitting` deaktiviert, `enterText` greift ohne eigene State-Choreografie nicht. Gehört in einen Folge-PR, zusammen mit den drei übrigen Goldens dieser Datei, die `message:` ohne Tippen setzen (`attached`, `error_snackbar`, `success_snackbar`). - **Golden `attached`**: hat keinen Handbuch-Block. Von 302 macOS-Baselines sind 279 im Mapping von `assemble-handbook-screenshots.sh`, 23 nicht — es gibt also keinen Vollständigkeitsanspruch, und ohne Beschreibung auch keinen Widerspruch. Der Referenzgraph ist in beide Richtungen gezählt: 279 Mapping-Zeilen, 279 eindeutige Ziele, kein Ziel mit mehreren Quellen, kein totes Ziel. - **Engere Finder** (`find.widgetWithText`): trägt hier nicht, weil der Hinweistext nach dem Tippen verschwindet. Ein zweites Textfeld ließe `enterText` mit „Found 2 widgets" **laut** scheitern, nicht still durchlaufen. - **Drei vorbestehende Zeilennummern-Verweise** in Kommentaren derselben Datei (`cubit:44`, `page:40-48`, `page:49-56`) — alle drei zeigen inzwischen ins Leere. Vorbestand, eigener Vorgang. ## Schlusspass **Final pass (c984139):** **Coherent:** Der Zustand, den das neue Bild zeigt, ist der **einzige in Produktion erreichbare** — `updateMessage` hat in `lib/` genau einen Aufrufer (`support_create_ticket_page.dart:120`, `TextField.onChanged`), der Cubit wird an genau einer Stelle erzeugt (`page:23`) und startet immer mit leerem `message`; es gibt keinen Restore-, Draft- oder Deep-Link-Pfad. Die alte Baseline zeigte damit einen Zustand, den kein Nutzer je sieht. Das unkontrollierte `TextField` ist deshalb kein Fehler, den dieser PR verdeckt, sondern korrekt: es gibt keine zweite Quelle für den Text. Die Abweichung vom Repo-Muster ist begründet — `restore_wallet` kommt mit zwei `pump()` aus, weil `mnemonic_input_field.dart:43` `border: InputBorder.none` setzt und der sichtbare Rahmen dort ein statischer `Border.all` ist (`mnemonic_field_base.dart:25`); unser Feld hat einen animierten `focusedBorder` (`page:136-141`) und braucht darum `pumpAndSettle`. **Nothing extra:** Zurückgenommen wurden fünf Zutaten aus früheren Review-Runden (Senden-Knopf- Assertion, Chip-Assertion, Scheduler-Wächter, `precacheImages`, Konstante über alle Goldens); übrig bleibt der kleinste Eingriff, der Bild und Beschreibung in Deckung bringt. Die entfernte `pump()`- Zeile ist gegengeprüft: wieder eingefügt bleiben die Goldens 6/6 grün und der Pixelvergleich unverändert — sie war wirkungslos. Die beiden Assertions sind kein Übermaß neben dem Pixelvergleich: sie halten den Fehlermodus auch dann fest, wenn die Baseline später regeneriert wird und ein leeres Feld sonst stillschweigend einfrieren würde. Die Konstante koppelt genau die zwei Stellen, zwischen denen die Invariante gilt (`enterText` ↔ `state.message`); die vier Nachbar-Goldens haben kein `enterText` und bleiben deshalb bewusst unangetastet. **Sources closed:** Issue #896 (Weg 2 umgesetzt, Handbuch-HTML unangetastet); Commit-Messages decken den Diff; Review-Kanäle: 0 Reviews, 0 Inline-Kommentare, 1 Issue-Kommentar von TaprootFreak vom 07.08. — beantwortet mit den auf dieser SHA gemessenen Zahlen; die Rückfrage, ob ihm ein förmlicher Review-Eintrag oder etwas Inhaltliches fehlt, steht bei ihm. - **Konsistent logisch gebaut:** Der Test stellt genau den Zustand her, den das Bild zeigen soll; beide Assertions sichern je eine Eigenschaft, die sonst still falsch werden könnte. Der Kommentar beschreibt den Mechanismus — keine Zeilennummern, keine Nachbardateien, keine Begründung, die der Code nicht hergibt. - **Nicht zu viel gebaut:** Der Diff ist der Kern des Issues — `enterText`, `unfocus`, `pumpAndSettle`, zwei Assertions, eine Konstante, die Baseline. Fünf Zutaten aus früheren Review-Runden (Senden-Knopf-Assertion, Chip-Assertion, Scheduler-Wächter, `precacheImages`, Konstante über alle Goldens) wurden **zurückgenommen**, weil das Issue einen Eingriff verlangt, der nur diese eine Baseline trifft, und die fünf Nachbar-Goldens derselben Datei kein solches Gerüst tragen. Zwei Belege dafür, dass der Rückschnitt nichts aufgibt: die Goldens sind danach weiter 6/6 grün (keine der Zutaten hat je das Bild beeinflusst), und die beiden Eigenschaften ohne eigene Assertion hält der Pixelvergleich messbar — leerer `message` ergibt 4,98 %, ein anderer Chip 3,55 % Diff. Dazu eine belegt wirkungslose `pump()`-Zeile entfernt. - **Issue vollständig umgesetzt:** #896 nennt zwei Wege und tendiert zu Weg 2 („das Golden echt befüllen … danach passt die bestehende Beschreibung ohne Änderung"). Genau das ist umgesetzt: das Handbuch-HTML ist unangetastet, Block 251 stimmt jetzt mit seinem Bild überein. Die im Issue als „nicht betroffen" genannten Blöcke 250, 252, 258, 259 sind unverändert. - **Reviews umgesetzt oder begründet abgelehnt:** Eingearbeitet sind der Fokus-/Rahmenfehler, die entfernten Cross-File-Kommentarverweise, die tote `pump()`-Zeile und die falsche Begründung im Doc-Kommentar. Begründet abgelehnt und dokumentiert: engere Finder, Golden 252, Golden `attached`, die drei vorbestehenden Zeilennummern-Verweise, die Konstante in den vier Nachbar-Goldens. - **Veröffentlichter Inhalt geprüft:** Das PNG geht auf handbook.realunit.app. Kein QR-/Barcode (`zbarimg`, Exit 4), keine Zugangsdaten, keine Adressen — Formularfelder und ein Beispielsatz. - **Geprüft auf dieser SHA:** volle Suite `flutter test` — **5244 Tests, alle grün**, Exit 0, keine fehlgeschlagene Suite. Die Zahl ist gegen die CI quergeprüft, die dieselbe Menge in zwei Läufen fährt: 302 Golden-Tests (`flutter test test/goldens`) plus 4941 bestandene und 1 übersprungener Test (`flutter test --coverage --exclude-tags golden`) — zusammen dieselben 5244. `flutter analyze` ohne Befund. Drei Mutationsproben auf dem neuen Test, jede **5 grün / 1 rot**: anderer Text getippt (`Found 0 widgets`), `unfocus` entfernt (`Expected: false / Actual: <true>`), `pumpAndSettle` → `pump` (`Pixel test failed, 0.30%, 1000px`). Die Regenerierung ist einmal ausgeführt worden: lokal neu erzeugt ergibt denselben SHA256 `7c15a7a1f76b83fe…`, `git` sieht keine Änderung. Mechanischer Katalog-Check `tf-test.sh`: 0 Blocker. CI auf dieser Revision: 5 Checks grün, 1 übersprungen (Maestro), Annotationen gelesen — zwei Warnungen, beide Vorbestand (lcov meldet keine Function-Coverage; `actions/checkout@v4` auf Node 20).
EN: Copy icons on the bank-transfer details screen were a 16px tap target, so they were hard to hit on iPhone. They are now 44×44 IconButtons. IBAN still copies ungrouped; the remittance reference copies as well. DE: Die Kopier-Icons auf den Zahlungsdetails waren nur 16 Pixel gross und auf dem iPhone kaum treffbar. Sie sind jetzt 44×44. Die IBAN wird weiter ohne Leerzeichen kopiert, der Verwendungszweck ebenfalls. <details> <summary>Details</summary> Incident: a customer could not copy payment details from the RealUnit app into the banking app on iPhone. Change: `_PaymentDetailsRow` uses `IconButton` with a tight 44×44 constraint instead of a 16px `InkWell` around the icon. Tooltip uses `MaterialLocalizations.copyButtonLabel`. Tests in `payment_details_card_test.dart` assert the tappable control is at least 44×44 and that tapping the purpose-of-payment copy control writes the remittance reference. </details> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…ation gate (#908) EN: The buy-page CTA forwards the API's KYC context on the `registrationRequired` and `kycRequired` gates but not on the `primaryEmailNotConfirmed` one, so a re-check after confirming email was scored against the app's full KYC step set instead of the `RealunitBuy`-scoped subset. A buyer who already finished `Ident` could be reported `InProgress` for steps `RealunitBuy` deliberately excludes from the buy gate. This closes item 3 of DFXswiss/backend#4556. DE: Der Buy-Seiten-CTA reicht den von der API gelieferten KYC-Kontext bei den Gates `registrationRequired` und `kycRequired` weiter, beim Gate `primaryEmailNotConfirmed` bisher jedoch nicht — dadurch wurde eine erneute Prüfung nach der E-Mail-Bestätigung gegen den vollständigen KYC-Schritt-Satz der App statt gegen die `RealunitBuy`-Teilmenge bewertet. Ein Käufer, der `Ident` bereits abgeschlossen hatte, konnte dadurch als `InProgress` gemeldet werden wegen Schritten, die `RealunitBuy` bewusst vom Buy-Gate ausschliesst. Schliesst Item 3 von DFXswiss/backend#4556 ab. <details> <summary>Details</summary> ## Problem `PaymentActionButton`'s `primaryEmailNotConfirmed` branch pushed `AppRoutes.kyc` with no `extra`, unlike its `registrationRequired`/`kycRequired` siblings a few lines above, which already pass `extra: paymentState.context`. That alone wasn't the whole gap: `paymentState.context` was itself always `null` on this path. `BuyPaymentInfoFailure.context` is populated from a thrown exception's server-supplied field for the `kycRequired`/`registrationRequired` branches (`e.context`), but `primaryEmailNotConfirmed` is a `QuoteError` code embedded directly in the quote response body — a different shape with no context field to read. Losing the context here means `KycInfoMapper.toDto` (API side) computes `processStatus` over the full `requiredKycSteps` set instead of the `RealunitBuy`-scoped subset. Concretely: a buyer who already finished `Ident` but hasn't confirmed their primary email hits this gate, taps through to KYC, confirms the email, and on the subsequent re-check could still be reported `InProgress` — not because anything buy-relevant is outstanding, but because `FinancialData`/`DfxApproval` (steps `RealunitBuy` context deliberately excludes from the buy gate) are unfinished. ## Change Three parts: 1. `payment_action_button.dart` — forward `extra: paymentState.context` on the `primaryEmailNotConfirmed` push, mirroring the two branches above it. 2. `buy_payment_info_cubit.dart` — populate that context in the first place: the `primaryEmailNotConfirmed` `BuyPaymentInfoFailure` is now constructed with `context: 'RealunitBuy'`. Since this cubit is unconditionally the RealUnit buy flow, the value is a static call-site identifier rather than a server decision, hardcoded as the same literal already used for the sibling branches (`test/screens/buy/cubits/buy_payment_info_cubit_test.dart`'s existing `... with context` tests). 3. `payment_action_button_test.dart` — the mock KYC route builder previously discarded `GoRouterState` entirely, so the forwarding in (1) had no regression coverage; it now captures `state.extra` and the relevant test asserts it equals `'RealunitBuy'`. The `primaryEmailRequired` branch (routes to email capture, never to `/kyc`) is untouched — it has nothing to scope. ## Verification - `flutter analyze` — no issues. - `flutter test test/screens/buy/cubits/buy_payment_info_cubit_test.dart test/screens/buy/widgets/payment_action_button_test.dart` — 24/24 passed. - Mutation-tested the new widget assertion by temporarily reverting the `extra` forwarding in `payment_action_button.dart` and confirming the test goes red, then restored it. </details>
…code (#909) ## Summary - The phone number field on the registration and personal-data steps was hard-limited to a closed `+41` / `+49` dropdown, blocking customers with other prefixes (e.g. Austria, Swiss citizens living abroad) from registering with their real number. - Replaces the dropdown with a free-text prefix field, prefilled `+41`, with a fixed non-deletable `+` prefix (`InputDecoration.prefixText`). The client checks format only (1 to 3 digits) and defers whether a given dial code actually exists to the API, per CONTRIBUTING ("the API decides") -- the national-number field right next to it already follows that rule. - Restores `+423` (Liechtenstein) to the seeded-value decomposition list with longest-first matching, so a stored `+423...` number is no longer silently misread as `+41` on re-entry. - Updates the twin test assertions that referenced the old dropdown (widget count, `find.text`) and drops the now-orphaned phone-prefix-dropdown golden case and its handbook references. **Symptom (verbatim):** Product owner forwarded a customer-facing report: Austrian customers and Swiss citizens living abroad cannot register with their real phone number, because the country-prefix dropdown only offers `+41` and `+49`. **Scale:** At least one concrete customer report. The affected class is every registration outside CH/DE -- explicitly named: Austria and Swiss citizens abroad (potentially any country). The prior dropdown covered 2 of roughly 200 possible international dial codes. **Smaller fix considered:** Adding a few more hardcoded entries to the closed dropdown list (e.g. `+43`, `+423`) was rejected -- it would still be a closed, arbitrarily-bounded list needing a follow-up PR for the next missing country, repeating the same complaint. A full country-code picker package (search UI, flags) was also rejected as unnecessarily large for this fix: new dependency, and a second country-list data source parallel to the existing DFX-API-backed one already used for nationality. ## Test plan - [x] `flutter test test/widgets/form/phone_number_field_test.dart test/screens/kyc/steps/kyc_personal_data_page_test.dart` -- 25/25 green (12 + 13) - [x] Counter-check 1 (dropdown removal + twin-spot fixes): 11/23 red without those changes - [x] Counter-check 2 (1-3 digit widening on top): 3/11 red in `phone_number_field_test.dart` alone without that specific change - [x] Counter-check 3 (empty-prefix required-error test): 1/12 red when the validator's empty check is mutated out - [x] `flutter analyze` on all touched Dart files -- no issues - [x] Verified live in the iOS Simulator: field shows `+41` by default, accepts any prefix from 1 to 3 digits (e.g. `+43`, `+423`), `+` cannot be deleted, a 4th digit is rejected - [x] Golden-image baselines regenerated via the `golden-regenerate` workflow (11 PNGs, bytes verified against the bot commit before taking over as a signed commit) -- all CI checks green: Analyze & Test, Visual Regression, Build handbook image + container smoke, BitBox quirks audit, Coverage Floor Gate. **Final pass (25cb745):** **Coherent:** All 5 commits serve the one title -- open the phone-prefix field. Commit 1 is the feature itself; commits 2-3 are direct fixes to findings from two independent reviews of that same feature (dial-code range, missing required-prefix test); commits 4-5 are the necessary consequences of the UI change (golden baselines, handbook screenshot count) -- no unrelated file is touched. **Nothing extra:** No new dependency, no country-picker package, no changes to `dropdown_field.dart`/`country_field.dart` or any of the three `PhoneNumberField` callers. The smallest fix (dropdown to free-text field) was widened from 2 to 1-3 digits only because review found it excluded real dial codes and violated CONTRIBUTING's "the API decides" -- not for speculative completeness. **Sources closed:** No linked GitHub issue (reported via a forwarded customer email, not a tracked issue). All three review channels checked via API (`pulls/909/reviews`, `pulls/909/comments`, `issues/909/comments`) -- all empty, no external feedback pending. Two independent judgment passes (TaprootFreak-catalog reviewer + Conformity/Logic pair) each found one real issue; both are fixed in commits 2 and 3 respectively and re-verified.
…919) EN: The handbook PRD deploy failed because the default token cannot clone the mail-preview source repository. This change checks that repository out with a read-only deploy key, drops those credentials after checkout, and fails loud if the secret is missing. DE: Der Handbook-PRD-Deploy ist fehlgeschlagen, weil der Default-Token die Mail-Preview-Quelle nicht klonen kann. Der Checkout läuft über einen read-only Deploy-Key, die Credentials werden danach verworfen, und ohne Secret bricht der Job klar ab. <details> <summary>Details</summary> The `Handbook CI/CD` job `deploy-prd` failed on push to `staging` at `Check out DFXswiss/api at develop` with HTTP 404. That step is not part of the promotion PR's review CI; Analyze & Test on the auto-PR stayed green. `handbook.yaml` now requires `HANDBOOK_API_CHECKOUT_SSH_KEY`, rejects an empty value, passes it to `actions/checkout@v4` as `ssh-key`, and sets `persist-credentials: false` so later steps do not keep that key. `handbook-deploy.yaml` forwards the repo secret. The PR-only handbook build check is unchanged and still does not clone that source. The deploy key is read-only. `RealUnitCH/web` stays on the default token. </details>
…920) EN: The handbook production deploy failed because the web-baseline guard still expected 22 Playwright PNGs after RealUnitCH/web added 14 for `/account-merge/`. This change raises `EXPECTED_WEB_BASELINE_COUNT` to 36 and adds matching cards in `#spec-web`. The cards describe adding a wallet address, not merging two accounts. After this lands on staging, the staging-to-develop promotion can run that deploy again. DE: Der Handbook-Produktions-Deploy ist fehlgeschlagen, weil der Web-Baseline-Guard noch 22 Playwright-PNGs erwartet hat, nachdem RealUnitCH/web 14 für `/account-merge/` ergänzt hat. Diese Änderung setzt `EXPECTED_WEB_BASELINE_COUNT` auf 36 und ergänzt die passenden Karten in `#spec-web`. Die Karten beschreiben das Hinzufügen einer Wallet-Adresse, nicht das Zusammenführen zweier Konten. Sobald das auf staging liegt, kann der Promote staging→develop den Deploy erneut fahren. <details> <summary>Details</summary> The auto-promotion PR #904 (`Promote: staging -> develop`) is red on `deploy-prd / Build and deploy to PRD` only: ``` Expected exactly 22 web baseline PNGs in _web-checkout/tests/__screenshots__, got 36. ``` Analyze & Test, Visual Regression, Coverage Floor Gate, and the PR-only Handbook Build Check stayed green: that last job does not clone RealUnitCH/web, so it never sees the count. Cause: RealUnitCH/web#25 committed 14 new Playwright baselines for `/account-merge/` (add a wallet address to an existing account). The previous handbook update of this guard was #862 (18 → 22, no-registration confirm states). There was no open PR covering 22 → 36. New files (14): | Viewport | Files | |---|---| | desktop-chromium | merge-confirmed(.png + -en.png), merge-invalid(.png + -en.png), merge-already-completed(.png + -en.png), merge-unavailable(.png + -en.png) | | tablet-chromium | merge-confirmed.png, merge-invalid.png (no tablet shots exist for already-completed / unavailable) | | mobile-safari | merge-confirmed-mobile.png, merge-invalid.png, merge-already-completed.png, merge-unavailable.png | Same markup as the existing confirm cards in `#spec-web`. Intro and `docs/handbook/README.md` now name `/account-merge/` next to `/confirm-aktionariat`. No Dart, no golden regeneration, no other workflow files. </details>
Promote: staging -> develop
…tory filter (#907) EN: The transaction-history filter compared the end bound against the picked day at local midnight, so every transaction timestamped later on that day was dropped (#779). The bound is now the start of the following local day, compared exclusively, which makes the selected end day fully inclusive. Three regression tests cover the same-day-afternoon case, the day after the bound, and a UTC-flagged timestamp falling on the local end day. DE: Der Transaktions-Filter verglich die obere Grenze gegen den gewählten Tag um lokale Mitternacht, wodurch jede später an diesem Tag datierte Transaktion herausfiel (#779). Die Grenze ist jetzt der Beginn des Folgetags in lokaler Zeit, exklusiv verglichen, womit der gewählte Endtag vollständig enthalten ist. Drei Regressionstests decken den Nachmittagsfall, den Tag nach der Grenze und einen UTC-markierten Zeitstempel am lokalen Endtag ab. <details> <summary>Details</summary> ## Root cause `DatePickerField` → `DatePicker.pickDate` returns the selected day as `DateTime(y, m, d)`, i.e. **local midnight**. `TransactionHistoryFilterCubit._applyFilter` treated that value as the inclusive upper bound: ```dart final beforeEnd = endDate == null || !transactionDate.isAfter(endDate); ``` For an end date of `2026-04-01` that bound is `2026-04-01 00:00:00.000` local, so a transaction at `2026-04-01 14:00` satisfies `isAfter(bound)` and is excluded. Only transactions at exactly 00:00:00.000 on the selected end day survived — an off-by-one day on the upper edge of every range the user picks. The start bound is not affected: local midnight is already the correct *start* of the picked day, so a picked start date behaves as intended. ## Fix ```dart final endBound = endDate == null ? null : _startOfNextLocalDay(endDate); ... final beforeEnd = endBound == null || transactionDate.isBefore(endBound); ``` with ```dart DateTime _startOfNextLocalDay(DateTime date) { final local = date.toLocal(); return DateTime(local.year, local.month, local.day + 1); } ``` Two deliberate details: - **Exclusive next-day bound rather than `23:59:59.999`.** `DateTime` carries microsecond resolution, so an end-of-day sentinel would still leave a sub-millisecond hole. `isBefore(startOfNextDay)` has no gap. - **`DateTime(y, m, d + 1)` rather than `.add(const Duration(days: 1))`.** `Duration` adds 24 absolute hours; on a DST-transition day that lands at 23:00 or 01:00 local instead of the next local midnight. The constructor normalises overflowing day values and resolves the wall-clock instant correctly, including across month and year ends. ## The zone concern raised in the issue Checked and **not a defect**: `DateTime.isBefore` / `isAfter` compare `microsecondsSinceEpoch`, i.e. absolute instants, so a UTC-flagged `timestamp` (from `DateTime.parse` on an API `...Z` string) is compared correctly against a local bound — no cross-boundary drift. The trap the team has hit before is `DateTime.==` / equality, which additionally compares the `isUtc` flag; this comparison path never uses it. What *does* matter for zone consistency is which calendar day the bound denotes. `TransactionHistoryRow` renders `transaction.timestamp.toLocal()`, so the user reads local days; the bound is therefore normalised via `toLocal()` before the day components are taken, so the filtered range matches the days shown in the list. The added `toLocal()` is a no-op for the values the picker produces and guards the bound against a caller passing a UTC-flagged `DateTime`. ## Blast radius `state.filtered` also feeds `TransactionHistoryDownloadButton`, so the exported statement inherits the same correction — a statement generated with an end date of "today" previously omitted everything from today. ## Tests Added to `test/screens/transaction_history/cubits/transaction_history_filter_cubit_test.dart`: 1. `changeFilter keeps transactions timestamped later on the selected end day (issue #779 regression)` — 00:00, 14:00 and 23:59:59 on the end day; expects all three. Fails on the old code with 1 of 3. 2. `changeFilter still excludes the day after the selected end date` — guards against over-correcting the bound by a day. 3. `changeFilter includes a UTC-flagged timestamp falling on the local end day` — pins the instant-vs-flag semantics described above. One existing test name (`changeFilter includes the boundaries (isBefore / isAfter, not isAtSameMoment)`) was shortened to `changeFilter includes the boundary days`, since it no longer describes the implementation it names. Its assertions are unchanged, and all four pre-existing filter tests keep passing under the new bound. **Not executed locally:** no Flutter toolchain was available in the environment this change was prepared in, so `flutter test` and `flutter analyze` have not been run against it — CI on this PR is the first execution. The diff was reviewed by hand against the existing test file's structure and the repo's `page_width: 100` formatter setting. </details>
EN: API error text is shown as-is. The app no longer substitutes local copy for quote, confirm, sell, send, or KYC failures. Hardware and cancelled-signature messages stay local. DE: API-Fehlertexte werden unverändert angezeigt. Die App ersetzt sie bei Quote, Confirm, Sell, Send und KYC nicht mehr durch lokale Texte. Hardware- und Abbruch-Meldungen bleiben lokal. <details> <summary>Details</summary> Confirm snackbar: `BuyConfirmFailure.message` from the API. Quote 503 / unknown: `BuyPaymentInfoFailure.message` from the API; the info card no longer uses `priceProviderUnavailable*` / `paymentInformationFailed*`. Sell 503 snackbar: `state.message` instead of `priceProviderUnavailableTitle`. Send: API `message` is the sheet body when present; ARB remains only for BitBox/signature-cancelled. KYC snackbars (email, nationality, personal data, link wallet, registration, 2FA, ident error): API `message` as-is. Depends on the backend PR that returns user-facing `message` fields. </details> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Promote: staging -> develop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automatic Release PR
This PR was automatically created after changes were pushed to develop.
Commits: 7 new commit(s)
Checklist