Skip to content

c638b50e - Label the invoice screen for the payer when opened with ?pay - #1274

Draft
joshuakrueger-dfx wants to merge 26 commits into
DFXswiss:developfrom
joshuakrueger-dfx:feat/invoice-payer-wording
Draft

c638b50e - Label the invoice screen for the payer when opened with ?pay#1274
joshuakrueger-dfx wants to merge 26 commits into
DFXswiss:developfrom
joshuakrueger-dfx:feat/invoice-payer-wording

Conversation

@joshuakrueger-dfx

@joshuakrueger-dfx joshuakrueger-dfx commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

EN:
A pay query parameter switches /invoice to payer wording, keeps that mode across a reload, and renders a printed payee as text instead of an editable field; it also closes a path that let a printed QR carry a foreign lightning value into the payment page. The branch is rebased onto current develop (b3052bd) instead of carrying a merge commit, and the seventeen reviewed commits are unchanged in content. The commits after them answer review passes over the finished branch, my own and one from a second model family: a payment answer that no longer matches the form cannot reach the payer, the payee field carries an accessible name and announces its error state, a fifth handbook baseline covers the typed and verified payee, and a new test drives the screen through the real navigation hook to prove no foreign parameter reaches the payment page. No CI check has run on this head — this is a draft from a fork, and the full-stack stage cannot run on a fork at all; both are open items below.

DE:
Ein pay-Query-Parameter stellt /invoice auf die Sprache des Zahlers um, hält diesen Modus über einen Reload und zeigt einen gedruckten Empfänger als Text statt als editierbares Feld; ausserdem wird ein Pfad geschlossen, über den ein gedruckter QR-Code einen fremden lightning-Wert in die Zahlungsseite tragen konnte. Der Branch ist auf das aktuelle develop (b3052bd) rebased statt über einen Merge-Commit integriert, die siebzehn reviewten Commits sind inhaltlich unverändert. Die Commits danach beantworten Review-Pässe über den fertigen Branch, eigene und einen aus einer zweiten Modellfamilie: Eine Zahlungsantwort, die nicht mehr zum Formular passt, erreicht den Zahler nicht, das Empfängerfeld trägt einen zugänglichen Namen und meldet seinen Fehlerzustand, eine fünfte Handbuch-Baseline deckt den getippten, bestätigten Empfänger ab, und ein neuer Test führt den Screen durch den echten Navigations-Hook und belegt, dass kein fremder Parameter die Zahlungsseite erreicht. Auf diesem Head ist kein CI-Check gelaufen — es ist ein Draft aus einem Fork, und die Full-Stack-Stufe kann auf einem Fork grundsätzlich nicht laufen; beides steht unten als offener Punkt.

Details

Not symptom-driven: No incident and no error report behind this. It came out of reading the screen together with the product owner while setting up printed-invoice QR codes for a merchant.
Scale: The screen is the payment entry point for every OpenCryptoPay payer; the first merchant printing this QR bills more than 100 invoices a month, each one sending a customer here.
Smaller fix considered: Reusing the existing recipient parameter as the payer signal, which would add no new parameter at all — insufficient because the documentation defines recipient as a convenience for the merchant to pre-fill the form, so it does not identify who is looking at the screen and would relabel the merchant flow as well.

Open items — none of these can be closed from this branch

What has to happen Why not from here Who can do it
Let the checks run on this head. As a draft, PR CI, CodeQL, the review bot and the handbook job all record skipping. A draft runs CI only with the ci or ci:full label, and applying a label needs write access — this account has read access only (AddLabelsToLabelable is refused). Marking it ready would not help either: ci-on-ready cannot dispatch a fork head. anyone with write access, by applying ci
Run the full-stack suite on this head. Not a label question. e2e-stack.yml probes E2E_API_CHECKOUT_KEY before it looks at the label, and a fork pull request never receives repository secrets, so it takes mode=none even with ci:full. It needs a branch inside this repository or a manual workflow_dispatch. repository maintainers
Re-review. Both open change requests are addressed, but they sit on heads that no longer exist. An author cannot submit a review of their own pull request. @TaprootFreak, @marassteiner
Grant or refuse an exception for the two newly reported defects below — the confirmation tick, and the query that now survives in payer mode. Both granted by marassteiner on 2026-09-03. marassteiner
Grant or refuse the written exception for the SDK rule. Granted by marassteiner (review of 2026-08-12, restated in four later reviews). marassteiner
Decide on the five pre-existing defectsdeferral granted; tracked as #1345#1349. marassteiner
Merge DFXswiss/packages#201 (feat/payment-link-pay-contract). Different repository; it carries the response type the SDK method needs. packages maintainers
Merge DFXswiss/packages#206 — the SDK method for the hand-built call. It is open, not a draft, and until #201 lands it carries #201's two commits inside its own diff. Different repository. packages maintainers
Publish @dfx.swiss/react with that method. Release process, not a branch. packages maintainers
Open the consumer pull requestrefactor: use the SDK invoice payment call. Needs the published version first; this repository keeps consumer work in its own pull request (#1239#1243 are the open examples). me, once the row above is done

Two issues can close when this lands: #1364 (payer flow in the full-stack harness) and #1365
(reality declaration for the stubbed recipient lookup). Both are on this branch.

Declared deviation

Per Deviating from these guidelines, stated explicitly rather than left to be discovered:

This pull request does not meet "API access goes through the SDK." invoice.screen.tsx still
builds the URL for GET /v1/paymentLink/payment by hand and fires it with fetchJson.

Reason: the guideline's own remedy — add it to the SDK, release it, then consume it — has a
step no branch in this repository can perform. @dfx.swiss/react has no method for this endpoint;
the SDK side is written and open as DFXswiss/packages#206. Until it lands and a version is
published, there is nothing here to consume, and working around it at the call site is the specific
thing the guideline rules out. The exception was granted in writing by marassteiner.

Context

A merchant wants one static QR code printed on every paper invoice: the customer scans it, types
the invoice number and the amount, and pays in crypto. app.dfx.swiss/invoice?recipient=<route label> already supports that. What it was never built for is a payer standing in front of it —
every label on that path was written for the merchant creating a link.

Change

pay switches the screen to payer wording ("Pay invoice", "Invoice number", "Invoice amount",
"Continue to payment") plus a line asking the payer to copy both values from the invoice. Without
the parameter every existing string stays byte-identical. The printed QR then carries
…/invoice?recipient=<label>&pay=1.

One thing changes for the merchant without a string changing, which is why qr-code.tsx is in this
diff: the placeholder shown while the link is still being built used to be a real QR code drawn at
near-zero contrast — invisible to zbarimg, readable by a phone camera. It is now a plain
skeleton.

The screen used to clear the query on mount, so a reload, a tab restore or a back navigation
dropped pay and fell back to merchant wording while autofill restored the values — it looked as
if only the labels were wrong. The query is now kept in payer mode and still cleared in merchant
mode.

Keeping it made every other parameter of the opened URL travel onwards, which is a real hijack
path: payment-link.context.tsx resolves lightning through Lnurl.decode before it looks at the
invoice at all. "Continue to payment" therefore navigates with the object form of navigate and
the new replaceParams: true, so the target query is an allowlist of routeId/route, amount,
message and expiryDate and inherits nothing. The option is additive and off by default; every
other call site merges exactly as before.

Review history

Round 1 raised eight findings: first painted frame, retained query in the payment request,
incomplete wording switch, German form of address, pay parsed as a blocklist instead of an
allowlist, a test that tested neither reload nor back, a freely editable printed recipient, and the
SDK rule. Seven are fixed here; the eighth is the declared deviation above.

Round 2 (TaprootFreak, 2026-08-13) asked for the payer flow in the full-stack harness. Three
payer cases are in e2e-stack/specs/payment-links.spec.ts since then.

Round 3 (marassteiner, 2026-08-26) found one defect: inserting the invoice-screen reality
declaration split a pair of collection-invoice bullets in docs/test-architecture.md, so "the same
two specs" pointed at the wrong tests. Fixed — the entry now reads Both collection-invoice specs
also fabricate the invoice rejection
. Two codex-pr lanes reported on that head that the Codex
vendor could not run on that instance, explicitly not a code defect.

After that, my own review passes over the finished branch found what the later commits fix.
No reviewer raised these; they are listed so nobody has to guess where they came from.

  1. A payment answer could outlive the values it was built from. The screen validates on
    debounced form values while the button is gated on live validity, so editing an already
    validated amount, invoice number or recipient left the button active with the previous callback
    — the payer could send values the screen no longer showed. Changing any of the three now clears
    the callback, and an answer may only write once the values it was sent with still match the
    form. The guard covers the failure paths too, so an error message about an amount the payer has
    since replaced neither appears nor lingers.
  2. The displayed payee carried its error state in the text colour alone. It now sets
    aria-invalid, points at the explanation through aria-describedby, and the explanation
    announces itself. Its label id comes from useId(), so two mounted instances (the app is also
    mounted as a widget) cannot collide on a fixed DOM id, and the confirmation tick in the editable
    branch got the accessible name the display branch already had.
  3. The handbook missed a visual variant this change introduces — payer mode without a printed
    recipient, the payee typed and confirmed, invoice fields unlocked with the currency prefix.
  4. Two assertions were weaker than their titles, the invoice number was the one form dimension
    without a case of its own, the seven new translation keys had no check that all three languages
    carry them, and the files this branch adds had drifted from .prettierrc — CI runs no Prettier
    check for TypeScript.
  5. Nothing drove the screen through the real navigation hook. The screen test mocks it and can
    only observe the intent; the hook test runs without the screen. A new test starts on a printed
    payer URL that also carries lightning and merchant, clicks "Continue to payment" and asserts
    the exact key set that arrives at /pl.
  6. The payer's payee field had no accessible name. StyledInput renders its visible label
    without htmlFor and the input without an id, so a field is named by its placeholder — and
    payer mode deliberately drops that placeholder. A screen reader met an unnamed textbox on the
    one screen written for that user. The field now carries its label as an accessible name, and
    the test mock no longer invents the label association the real component does not make.

Reported defects that stay open

Per Report every bug you find, including pre-existing ones. None is fixed here. The deferral for
the first five was granted in writing; the last two are new and are the fourth open item above.

  1. validateRecipient has no guard against superseded responses. It fires per debounced
    keystroke with no request token. If an earlier answer arrives last, the tick and the currency
    belong to a name the user has already replaced, while the payment call stays blocked because it
    requires the names to match — a form that stalls with no error. invoice.screen: guard validateRecipient against superseded responses #1345.
  2. fetchJson discards the HTTP status. The only failure criterion is an error field, so a
    4xx body without one takes the success path. payment-link.context.tsx checks statusCode for
    the same endpoint — the two consumers disagree. invoice.screen / fetchJson: HTTP status discarded on paymentLink/payment GET #1346.
  3. clearParams is silently ignored in the string form of navigate. NavigationLink passes
    clearParams: ['lightning', 'key'] and is therefore a no-op. navigation.hook: clearParams is a no-op for string navigate (NavigationLink) #1347. Newly relevant here:
    because payer mode keeps the query, those values now survive a menu navigation instead of dying
    at the mount clear. No menu target consumes them today (PaymentLinkProvider is mounted only
    under /pl), so this is exposure in the URL and in browser history, not a proven takeover.
  4. isSafeRedirectUri accepts any https target with no host allowlist, and the parameter is
    stripped from the address bar afterwards, so the payer sees a clean URL. isSafeRedirectUri: any https target accepted (no host allowlist) #1348.
  5. The recipient name is interpolated into the error text before parsing, so <strong>,
    <br>, <i> and <p> become real elements. Not XSS — unknown tags are escaped and injected
    attributes dropped — but an attacker can place a bold sentence inside what reads as a DFX error
    message. invoice.screen: Trans interpolates recipient with escapeValue false (markup in error) #1349.
  6. New: the confirmation tick proves the route exists, not who owns it. validateRecipient
    uses only the currency from the answer; the payee shown is the string from the URL. With a
    numeric value the screen sends routeId and displays "Payee: 42 ✓" — a payer cannot tell whom
    they are paying, and the tick carries an accessible name that says "verified". Fixing it needs a
    display name from the API, so it is not a change this repository can make alone.
  7. New: in payer mode a foreign query survives for the rest of the session. Before this branch
    the mount clear removed every unknown parameter unconditionally. replaceParams: true protects
    the continue button only; see point 3.
  8. New: every recipient lookup failure reads as "recipient not recognized". A timeout, an
    offline browser, a 401 or a 500 produce the same business-level message as a genuine 404 and
    disable the flow, which blames a valid printed recipient and gives the payer no accurate retry
    signal. Pre-existing, and found by the second model family.

Handbook

The screen had no representation at all. It now has its own spec, its own group key and five
committed baselines: merchant, payer with an empty payee, payer with the payee printed and
rejected, payer with the payee printed and confirmed, and payer with the payee typed and confirmed.
All five were checked for scannable codes and readable secrets with zbarimg and with Apple Vision
(barcodes plus OCR over the whole image): no barcode in any of them, no credential in the
recognised text.

Named so the gap is a number rather than a silence: six further states of this screen exist that
this change does not introduce and that no baseline covers — the merchant side with an accepted
recipient, a validated payment, the payment error hint, and the German, French and Italian
renderings.

Coverage

Per file, for every file this pull request touches that Jest instruments. collectCoverageFrom is
src/**/*.{ts,tsx,js,jsx} minus src/**/*.d.ts; the runner excludes test files, and translation
JSON, the e2e specs and the baselines carry no coverage. Measured on this head with the full suite:

File Stmts Branch Funcs Lines
src/screens/invoice.screen.tsx 100 100 100 100
src/hooks/navigation.hook.ts 100 100 100 100
src/components/payment/qr-code.tsx 100 100 100 100

On develop (b3052bd) all three sit at 0 / 0 / 0 / 0 — measured there with the full suite,
not inferred: no test on develop executes any of them.

Not verified

  • The path beyond "Continue to payment" through to a settled transaction, and the QR printed on
    paper and scanned with a real camera.
  • Whether the dev API keeps rejecting the placeholder name used in the rejected-payee baseline; if
    a route with that label ever exists there, that image changes.
  • That the .finally of a superseded request leaves the loading state alone while a newer request
    is still in flight: the mocks in this suite cannot hold two requests open at once, so that one
    line is covered by no test. Everything else the later commits added is pinned by a test.
  • One test outside this diff (src/__tests__/realunit.screen.test.tsx, number formatting) fails
    locally on macOS; it fails identically on unmodified develop, so it is not this branch.

Note for later

The app2 rewrite carries its own invoice screen on feat/app2-preview; it will need the same
wording so the two do not drift apart.

@joshuakrueger-dfx
joshuakrueger-dfx marked this pull request as ready for review August 6, 2026 10:31
@TaprootFreak

Copy link
Copy Markdown
Contributor

Review — full run

Two independent review lanes (conformity + logic), every finding re-verified against the source. Measured locally on 4fb0e52 with Node 20: ESLint exit 0, 73 suites / 794 tests green. Reverting invoice.screen.tsx to the pre-PR state turns exactly the two payer cases red (2 of 6) — the new tests really do hang on the change.

Bugs introduced by this PR

1. The first painted frame shows merchant wording in payer mode.
isPayerMode is state plus a passive useEffect (src/screens/invoice.screen.tsx:54,68-76), so the initial render commits with false. Measured title sequence for /invoice?recipient=Foo&pay=1: ["Create Invoice", "Pay invoice", "Pay invoice"]. The value is derivable from urlParams on every render — in payer mode the query is deliberately not cleared — so state plus effect is unnecessary. Repo precedent for the direct derivation: src/screens/mail-login.screen.tsx:19. The new tests cannot see this: useLayoutOptions is mocked and every assertion sits behind waitFor, which is a no-op here because RTL flushes passive effects inside render().

2. The retained query string is carried into the payment request.
navigate(string) merges the current location.search into the target (src/hooks/navigation.hook.ts:32relativeUrl, src/util/utils.ts:64-81). In payer mode location.search is no longer cleared, so "Continue to payment" navigates to /pl?routeId=…&amount=…&message=…&expiryDate=…&recipient=Foo&pay=1, and src/contexts/payment-link.context.tsx:200-206 builds the API URL from the complete urlParamsrecipient and pay are sent to GET /v1/paymentLink/payment. The API drops them (global whitelist: true without forbidNonWhitelisted, and CreateInvoicePaymentDto knows neither field), so nothing breaks today, but the coupling is unintended, it was impossible before this PR, and the merchant's route label now appears in the request log.

3. The wording switch is incomplete.
The QR code and the "Copy Link" button (invoice.screen.tsx:151-160) are merchant tools and are not tied to isPayerMode. In payer mode they sit above the form as a loading placeholder until the payer has filled in both fields — the most prominent element on a screen that is supposed to read as "pay this bill". The recipient field (:172-182) also keeps the merchant label "Recipient" and the placeholder "John Doe".

4. The new German string breaks the file's form of address.
src/translations/languages/de.json renders the hint as an impersonal infinitive ("Rechnungsnummer und Rechnungsbetrag genau so eingeben, wie sie auf der Rechnung stehen"), while every other instruction in that file uses the capitalised Du form plus imperative (e.g. "Bitte gib ihn hier ein.", "Um zu handeln, gib bitte Deine E-Mail-Adresse ein."). fr.json and it.json are consistent with their neighbours.

5. pay is parsed as a blocklist instead of an allowlist.
pay !== null && pay !== '0' && pay !== 'false' (invoice.screen.tsx:70-71) enables payer mode for ?pay=no, ?pay=False, ?pay=off and the empty ?pay=. Exact string comparison, no toLowerCase(). An allowlist (['1','true','yes']) matches what the documented pay=1 promises.

6. The test named "survives reload / back" tests neither.
src/__tests__/invoice-screen-payer-wording.test.tsx:243-253 renders once and reads router.state.location.search — no router.navigate(-1), no remount, no reload. It asserts the initial state it was handed.

Pre-existing bugs in the files this PR touches

These predate the diff. Reporting them as bugs, not as observations — a bug does not get less severe for being older.

7. The prefilled recipient field is freely editable.
invoice.screen.tsx:172-182: the recipient comes from the printed QR via setValue and has neither disabled nor readOnly. A payer standing at the counter can overwrite Foo and pay a different recipient than the one the merchant printed on the invoice. With ?pay this screen is now explicitly aimed at that payer, which turns a latent problem into a live one.

8. The pay-request call bypasses the SDK.
invoice.screen.tsx:36,127 builds an API URL by hand from Api.url and fires it with a raw fetch (fetchJson, src/util/utils.ts:291-294), while the same screen already uses usePaymentRoutes() from @dfx.swiss/react for getPaymentRecipient. Endpoint knowledge — verb, query shape, response type — lives in this repo and will silently go stale. PaymentRoutesInterface (SDK 1.7.0) has no method for the unauthenticated GET /v1/paymentLink/payment, so this belongs in DFXswiss/packages (packages/react/src/hooks/payment-routes.hook.ts) as an additive, optional-parameter change, released, then consumed here. Same pattern in src/contexts/payment-link.context.tsx:354,517.

Hard requirements for this PR

  • Self-contained. Every point above is fixed in this PR. No follow-up PR unless it is explicitly granted as an exception.
  • Handbook coverage must be 100 %. The handbook renders committed Playwright baselines plus scripts/handbook/metadata.json. e2e/payment-and-special.spec.ts covers /invoice but contains zero toHaveScreenshot assertions, and none of the 34 metadata entries describes the invoice screen — the screen this PR changes does not appear in the handbook at all. handbook-check.yaml does not even trigger, because its path filter matches none of the changed files. Both modes (merchant and payer) need a baseline and a metadata entry.
  • Test coverage must be 100 % on every file this PR touches. Currently src/screens/invoice.screen.tsx sits at 78.57 % statements, 69.38 % branches, 56.25 % functions, 76.92 % lines (--collectCoverageFrom='src/screens/invoice.screen.tsx'). The uncovered part is exactly where findings 2, 5 and 7 live: validatePayment, the callback navigation, and the error branches. useNavigation is fully mocked and mockNavigate is never asserted, so no test can currently observe what URL the payer is actually sent to.
  • Anything that belongs in the SDK goes into the SDK. See finding 8: the fix is a PR in DFXswiss/packages first, then consumption here — not a workaround at the call site.
  • Rebase. The branch is one commit behind develop (Render a missing KYC file date as a gap, not as an invalid date #1273).

Checked and dismissed

The missing Playwright baseline is not a CONTRIBUTING violation on its own: the rule says regenerate the screenshots your change affects, and no baseline covers this screen — there is nothing to regenerate. The gap is the handbook requirement above, which asks for baselines to be created. Two further lane findings did not survive verification: the empty dependency array does not freeze the mode on a query change (no code path navigates to /invoice with a changed query while the screen is mounted — the only internal caller is payment-routes.screen.tsx:790 without parameters), and the deep Form mock is not a foreign body (the same enrich pattern already exists in src/__tests__/buy-personal-iban-mismatch.test.tsx:88-104).

@joshuakrueger-dfx

Copy link
Copy Markdown
Contributor Author

@marassteiner please review

This is a PR, not an issue: 14 files against develop, head 3197f06, 6/6 checks green.

@joshuakrueger-dfx

Copy link
Copy Markdown
Contributor Author

@marassteiner please review

This is a PR, not an issue: 14 files against develop, head 3197f066, 6/6 checks green.

@marassteiner

Copy link
Copy Markdown
Collaborator

Sorry — I've reached my hourly job limit and can't take this on right now. I'll pick it up automatically once the limit resets.

@marassteiner

Copy link
Copy Markdown
Collaborator

EN: Working on this now — job dfxswiss__services__1274__pr-review.
DE: Ich arbeite jetzt daran — Auftrag dfxswiss__services__1274__pr-review.

@marassteiner marassteiner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — request changes

Verdict: Request changes (one merge-blocker). No approval until coverage on every touched instrumented file meets CONTRIBUTING.

Local run: LOCAL_RUN_OKnpm install (after npm ci lock/host mismatch unrelated to this diff), npm run lint exit 0, CI=true npm test -- --watchAll=false76 suites / 824 tests green, dev server (PORT=3001 BROWSER=none npm run start) listens and returns HTTP 200 for /, /invoice, /invoice?pay=1&recipient=test. Compile: “No issues found” (pre-existing source-map warnings only). CI on the PR is 6/6 green.

Diff review: Compliance and logic lanes both completed on 3197f066 vs develop. Logic: 0 defects. Compliance/product scope is sound aside from the coverage gate below.


Merge-blocker

1. CONTRIBUTING 100% coverage not met on two touched instrumented files

Severity: merge-blocker (mandatory CONTRIBUTING criterion)

CONTRIBUTING requires every file a PR touches that Jest instruments to reach 100% statements, branches, functions and lines. Measured on this head with the full suite:

File Stmts Branch Funcs Lines Uncovered
src/screens/invoice.screen.tsx 100 100 100 100
src/hooks/navigation.hook.ts 70.83 46.15 66.66 70 34, 55–62, 78 (navigate(number), goBack, setParams, clearParams())
src/components/payment/qr-code.tsx 0 0 0 0 whole module (mocked as QrBasic in invoice tests; real component never executed)

The PR description only reports numbers for invoice.screen.tsx and treats it as the sole instrumented touch. That understates the rule: both navigation.hook.ts and qr-code.tsx are under src/**/*.{ts,tsx} and were modified in this branch (including the loading skeleton that replaces the scannable placeholder).

Required before merge: Bring navigation.hook.ts and qr-code.tsx to 100% on all four metrics (or drop those files from the diff if the change can live without them). State the per-file table in the PR description. Suggested checks:

CI=true npm run test -- --watchAll=false --coverage   --collectCoverageFrom='src/screens/invoice.screen.tsx'   --collectCoverageFrom='src/hooks/navigation.hook.ts'   --collectCoverageFrom='src/components/payment/qr-code.tsx'

Written exceptions / deferrals (this review)

Per CONTRIBUTING, a declared deviation and deferred pre-existing defects need an explicit grant on the PR. Granted by this review:

  1. SDK deviation (finding 8 / declared deviation)exception granted.
    Hand-built GET /v1/paymentLink/payment via fetchJson in invoice.screen.tsx:136 may remain for this PR. Reason accepted: @dfx.swiss/react has no method yet; packages#206 is the proper path; consumer follow-up after publish is the established pattern (#1239#1243). Landing a local workaround is what the guideline forbids; keeping the call until the SDK ships is acceptable here. Please still land the consumer PR once packages#201/#206 publish.

  2. Pre-existing defects reported in the PR bodydeferral granted (do not block this PR). Tracked as:

    # Topic Issue
    1 validateRecipient race / superseded responses #1345
    2 fetchJson drops HTTP status on payment GET #1346
    3 string-form clearParams no-op (NavigationLink) #1347
    4 isSafeRedirectUri accepts any https host #1348
    5 Trans recipient markup with escapeValue: false #1349

What looks good (not blocking)

  • Payer mode derivation from the query on every render; first-paint payee fallback; query kept in payer mode / cleared in merchant mode.
  • Navigation allowlist via replaceParams: true (no query inheritance / hijack path) with real hook tests, not only mocks.
  • QR loading skeleton (no scannable placeholder URL); merchant QR/copy hidden in payer mode; payee as display text when from URL.
  • Handbook: e2e baselines + scripts/handbook/metadata.json entry for all four variants.
  • de/fr/it keys present for the new strings.
  • Logic review: 0 Mängel. Mutation-style evidence in the PR body is thorough for the invoice screen suite.

Follow-up only (already tracked / not grounds to block once coverage is fixed)

See issues #1345#1349 above. No additional style nits.


Summary for the author: Fix coverage on navigation.hook.ts and qr-code.tsx to 100%, update the PR body table, push. SDK exception and pre-existing deferrals are granted above. Re-request review after that.

No merge from this account.

joshuakrueger-dfx added a commit to joshuakrueger-dfx/services that referenced this pull request Aug 12, 2026
Review on DFXswiss#1274 blocked on CONTRIBUTING: every instrumented file the
branch touches must hit 100% on statements, branches, functions and
lines. invoice.screen.tsx already did; navigation.hook.ts and qr-code.tsx
did not. Cover the remaining hook surface (history navigate, goBack,
setParams, clearParams, setRedirect, string merge) and the real QrBasic /
QrCopy paths, including the loading skeleton that must not encode a
scannable URL.
@joshuakrueger-dfx

Copy link
Copy Markdown
Contributor Author

@marassteiner please re-review

Coverage merge-blocker from your previous review is addressed on head bba6b9d0:

File Stmts Branch Funcs Lines
src/screens/invoice.screen.tsx 100 100 100 100
src/hooks/navigation.hook.ts 100 100 100 100
src/components/payment/qr-code.tsx 100 100 100 100

Commit: test(invoice): bring navigation and QR coverage to 100%
CI on this head: 7/7 green (Build and test, Full-stack E2E, CodeQL, review, handbook, both Analyze jobs).

@marassteiner

Copy link
Copy Markdown
Collaborator

EN: Working on this now — job dfxswiss__services__1274__pr-review__r2.
DE: Ich arbeite jetzt daran — Auftrag dfxswiss__services__1274__pr-review__r2.

@marassteiner marassteiner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EN: Coverage merge-blocker is fixed on bba6b9d0; logic is clean, CI is green, and this review approves with two follow-up issues only.
DE: Der Coverage-Merge-Blocker ist auf bba6b9d0 behoben; Logik ist sauber, CI grün — Approve, nur zwei Follow-up-Issues.

Details

Head / gates

Item Result
Head bba6b9d0 (feat/invoice-payer-wording)
Base develop (GitHub: MERGEABLE; branch is ~18 commits behind)
Gate A — Konformität (Codex) complete; see findings assessment below
Gate A — Logik (Codex) complete — 0 Mängel
Gate B — local LOCAL_RUN_OK
CI on head 7/7 green (Build and test, Full-stack E2E, CodeQL, review, handbook, both Analyze jobs)

Previous merge-blocker — fixed

Measured locally on this head (Node 20, full suite also green: 77 suites / 837 tests):

CI=true npm run test -- --watchAll=false --coverage \
  --collectCoverageFrom='src/screens/invoice.screen.tsx' \
  --collectCoverageFrom='src/hooks/navigation.hook.ts' \
  --collectCoverageFrom='src/components/payment/qr-code.tsx' \
  --testPathPattern='invoice-screen-payer-wording|navigation-hook-clear-params|payment-qr-code-basic'
File Stmts Branch Funcs Lines
src/screens/invoice.screen.tsx 100 100 100 100
src/hooks/navigation.hook.ts 100 100 100 100
src/components/payment/qr-code.tsx 100 100 100 100

Also: npm run lint exit 0; npm start on port 43174 compiled ("No issues found") and responded HTTP 200.

Merge-blockers on this head

None. Recommendation: approve.

Written exceptions from the previous review remain in force (SDK hand-built GET /v1/paymentLink/payment; pre-existing defects #1345#1349).

Compliance findings assessed (not blocking)

Codex Konformität raised four potential blockers against current origin/develop CONTRIBUTING. Assessment:

  1. Rebase hygiene (~18 commits behind; overlapping de/fr/it.json + handbook metadata) — GitHub reports mergeable: MERGEABLE. Recommend rebasing onto current develop before merge for a clean translation/metadata integration, but this is not held as a merge-blocker while the PR remains mergeable without conflict.
  2. Full-stack E2E payer pathe2e-stack covers merchant /invoice only; payer mode is unit+visual covered. CONTRIBUTING uses "should" here. Follow-up: #1364
  3. Reality declaration for installRecipientRoute (e2e/invoice-screen.spec.ts) — develop CONTRIBUTING requires a declaration when introducing a fake. Uncertainty (left to human, not blocked as precaution): the declaration taxonomy/format lives in DFXswiss/api and is not fully specified in this repo; develop already contains many e2e/** page.route mocks without per-entry declarations; visual e2e does not run in CI and the handbook already notes a stubbed API. Tracked as: #1365
  4. Handbook baseline for the payee loading spinner — transient spinner during recipient validation is not treated as a separate product "mode" variant (merchant / payer / prefilled / verified already have baselines). Not a merge-blocker; not filed as an issue.

Logic (summary)

Payer mode derived from the query each render; first-paint payee fallback; query retained in payer mode / cleared in merchant mode; payment navigation uses object-form navigate with clearParams + replaceParams allowlist; merchant QR/copy hidden in payer mode; loading skeleton avoids a scannable placeholder. No logic defects found.

What looks good

  • Coverage suite for the previously under-covered navigation.hook and real qr-code component
  • Handbook baselines for all four intentional screen states + metadata entry
  • de/fr/it keys for the new strings
  • Thorough mutation-style evidence retained in the PR description

Local run evidence (Gate B)

node v20.19.4
npm ci          # OK under Node 20 (npm 10); Node 24 host rejected lockfile
npm run lint    # exit 0
CI=true npm test -- --watchAll=false   # 77 passed / 837 tests
BROWSER=none PORT=43174 REACT_APP_API_URL=https://dev.api.dfx.swiss npm start
  → webpack compiled, "No issues found", HTTP 200 on :43174

Recommendation

Approve. Please still rebase onto current develop before merge when convenient (translations + handbook metadata overlap). No merge from this account.

@TaprootFreak TaprootFreak left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EN: Changes required: the full-stack E2E suite still only covers merchant /invoice and does not open the new ?pay payer flow.
DE: Änderung zwingend erforderlich: Die Full-Stack-E2E-Suite deckt weiterhin nur Händler-/invoice ab und öffnet den neuen ?pay-Zahlerfluss nicht.

Details

Visual E2E and handbook look complete (spec + four baselines + scripts/handbook/metadata.json key invoice-screen).

The remaining gap is the full-stack layer.

E2E (e2e-stack) — not met

CONTRIBUTING.md § Full-stack E2E: a PR that changes a screen must bring or update the matching full-stack test. /invoice is claimed by e2e-stack/specs/registry/payment-links.tspayment-links.spec.ts.

That spec still only opens /invoice in merchant mode (Create Invoice, editable recipient). It never navigates to /invoice?pay=1 and never asserts payer copy, display-only payee, or the verified/unknown recipient states this PR introduces.

Required: extend e2e-stack/specs/payment-links.spec.ts (or add a dedicated spec and keep a single registry claim) so a test actually opens the payer flow and checks the new wording/states.

Unit 100% on invoice.screen.tsx, qr-code.tsx and navigation.hook.ts is not proven by a coverage run (CI has no gate). The PR does add tests for the new branches; if any of those three files is not yet at 100% on all four metrics, that is a second blocker — state the per-file numbers in the description.

joshuakrueger-dfx added a commit to joshuakrueger-dfx/services that referenced this pull request Aug 13, 2026
Review on DFXswiss#1274 blocked on CONTRIBUTING: every instrumented file the
branch touches must hit 100% on statements, branches, functions and
lines. invoice.screen.tsx already did; navigation.hook.ts and qr-code.tsx
did not. Cover the remaining hook surface (history navigate, goBack,
setParams, clearParams, setRedirect, string merge) and the real QrBasic /
QrCopy paths, including the loading skeleton that must not encode a
scannable URL.
@joshuakrueger-dfx
joshuakrueger-dfx force-pushed the feat/invoice-payer-wording branch from bba6b9d to 888ca5b Compare August 13, 2026 08:49
The previous round fixed the wrong half of the problem. Locking the prefilled
recipient stopped a payer from redirecting the payment, but StyledInput passes
disabled straight through to the input element, and a disabled input is not
focusable and gets skipped by screen readers. That is the one field on this
screen a payer has to be able to check before paying: who the money goes to.
Unmanipulable but unreadable is a trade, not a fix.

The real question was whether a form field belongs there at all. When the
recipient comes from a printed QR code it is information, not input, so the
screen now renders it as text with its label. Nothing to lock, nothing to hide
from assistive technology. The Controller stays registered, so the value keeps
flowing into validation and the button still enables.

The confirmation tick moves with it: it used to be positioned against the
height of the input and now sits inline next to the value, carrying an
accessible name rather than being decoration - it tells the payer that DFX
knows this recipient.

The merchant flow and payer mode without a recipient parameter keep the input
field unchanged. The prefilled baseline shows the unknown-recipient case, where
no tick appears, so it needed no regeneration - verified by a run without
--update-snapshots.
Keeping the query in payer mode - needed so a reload does not lose the mode -
made every other parameter of the opened URL travel to the payment page. The
object form of navigate builds its target from the current location.search and
only removes what clearParams names, so listing recipient and pay covered the
two parameters anyone thought of and nothing else. The payment page reads
lightning before it looks at the invoice at all, and resolves it to whatever
server the parameter points at; merchant lands even earlier. Neither is in the
global strip list.

A printed code carrying the real merchant plus a foreign lightning parameter
therefore showed the payer the genuine payee with its confirmation tick, took
the invoice number and amount, and then fetched the payment from somewhere
else. Before this branch it could not happen: the query was cleared
unconditionally and the string form ran on an empty search.

clearParams cannot fix this. It is a blocklist, and it runs after the values
are set, so filling it with the current keys would delete the real amount along
with the attacker's. The target now builds its query from the parameters it is
given, and inherits nothing - an allowlist, the same shape the review asked for
one level down where pay was parsed as a blocklist.

The option is additive and off by default, so every other caller keeps merging
exactly as before. The hook test now starts from a query carrying lightning,
merchant and a second routeId and asserts the exact key set that arrives.
…than it was

The component mock never passed rules through, while the real Form injects
rules[name]. No unit test had a validation rule registered, so isValid was
constant true: removing Required from the recipient, or dropping !isValid from
the button, left all tests green. The question this branch raises - does the
required rule survive when the field becomes text - was the one thing the suite
could not see. The mock now mirrors the real Form, and both mutations turn it
red.

The e2e assertion for "no payee input" queried a role that StyledInput never
exposes: it renders the label without htmlFor and the input without id, and in
payer mode the placeholder is undefined, so there is no accessible name to
match. It returned zero against the previous revision too. It now asserts on
the element itself, which the component gives a name attribute from its
autocomplete value.

The merchant baseline carried a scannable QR code of the loading placeholder -
the public URL of whatever environment generated it. zbarimg missed it because
the placeholder is drawn at near-zero contrast, so the check that was supposed
to clear the image could not see what a phone camera reads. The placeholder is
now a plain skeleton, and the same scan that found the code confirms all four
baselines are clear.

The verified recipient finally has a baseline: stubbing the endpoint the way
support-issue-receiver-iban.spec.ts already does in this repository shows the
tick and its translations, and takes the dev API out of the third test as well.
The payee display also shows a spinner while the check runs, which the replaced
input branch did and its successor had dropped.
The hook test file is the only one in this repository that runs the real
useNavigation - the other sixteen mock it. Moving all three of its tests onto
the new option left clearParams covered by nothing at all: turning its delete
loop into a no-op kept thirty tests across three suites green, where the same
mutation against the previous revision of this file turned one of two red. A
fourth test now drives the probe without the option and asserts that the two
named parameters are the ones missing from the target.

Two of the tests added with the fix claimed more than they checked. The one
titled after the hijacked query lived in the file that mocks useNavigation, so
its target search never carried the location query and its assertions held
without the fix too - only the shape of the mock call differed, which the
neighbouring tests already assert. It is gone; the hook test covers that case
for real. The other named the display branch in its title but ran its
assertions against a second render where that branch is inactive, and deleting
the setup left everything green. It now says what it tests.

The option also did nothing in the string form of navigate, which builds its
target through relativeUrl and never reaches addParams - so a future caller
would have kept merging without noticing. It applies in both forms now.
clearParams in the string form is deliberately untouched: it is a pre-existing
no-op, and fixing it would change what NavigationLink does, which belongs in
its own branch.
The value arrives through setValue in the mount effect, which runs after the
first render, so the payer briefly saw the label above an empty line while the
name sat in the query the whole time. The mode was already derived directly
from the query after the review asked for it; the displayed value was not, so
that fix only went half way.

The rendered value now falls back to the query parameter, which is what the
state carries a moment later anyway - the display branch only exists when that
parameter is set. The form state is untouched: the Controller stays registered
and the value still comes from setValue.

The test proves the first frame rather than the settled one: React Testing
Library flushes passive effects inside render(), so it can never see it. It
mounts through createRoot inside flushSync instead, which commits the DOM and
layout effects but not the effect that sets the value.
… fails

The container and its React root were only removed on the success path, so a
failing assertion left the mounted screen in document.body and every later test
in the file found its elements twice. Mutating the fallback it guards turned
nineteen of twenty-nine tests red, eighteen of them for that reason alone -
which makes the counter-check useless as evidence, because it looks like a
crash instead of the one failure it is. Cleanup now runs regardless, and the
same mutation turns exactly one test red.
Review on DFXswiss#1274 blocked on CONTRIBUTING: every instrumented file the
branch touches must hit 100% on statements, branches, functions and
lines. invoice.screen.tsx already did; navigation.hook.ts and qr-code.tsx
did not. Cover the remaining hook surface (history navigate, goBack,
setParams, clearParams, setRedirect, string merge) and the real QrBasic /
QrCopy paths, including the loading skeleton that must not encode a
scannable URL.
The merchant /invoice test never opened ?pay=1. Open the three payer
states this change introduces — wording, verified display-only payee,
and unknown recipient — against the real API.
… twice

Display-only payee puts the name in the group and in the error text, so
a page-wide getByText is a strict-mode collision. Scope the assertion
to the error paragraph.
… themselves

CONTRIBUTING requires the reality declaration to change in the pull request
that introduces a fake. The four invoice baselines fulfil
GET /v1/paymentLink/recipient with a static 200 or 404; the declaration said
nothing about it, so a green visual run read as proof of the lookup.
The entry said three payer tests; the one with an editable payee never
submits a recipient and triggers no lookup.
…e state

Editing an already validated amount left the continue button active with the
previous callback for the length of the debounce, so a payer could send an
amount the screen no longer showed. The live form values now clear the callback,
and validatePayment ignores the answer of a request that has been superseded.

The displayed payee carried its error state in the text colour alone. It now
sets aria-invalid and points at the explanation through aria-describedby, and
its label id comes from useId so two mounted instances cannot collide.
The payer mode reaches a fifth visual state that no baseline showed: no printed
recipient, the payee typed and confirmed, the invoice fields unlocked with the
currency prefix. The spec now takes that screenshot and the handbook entry names
it alongside the other four.
The loading skeleton was pinned only for non-SVG data, where the image branch
can never run; the SVG case is now covered as well. The goBack test asserted the
path and stayed silent about the query string it carries along.
@joshuakrueger-dfx
joshuakrueger-dfx marked this pull request as draft September 2, 2026 15:53
@joshuakrueger-dfx
joshuakrueger-dfx force-pushed the feat/invoice-payer-wording branch from 7f1fb5f to 1ba7fe3 Compare September 2, 2026 15:53
@joshuakrueger-dfx joshuakrueger-dfx changed the title Label the invoice screen for the payer when opened with ?pay c638b50e - Label the invoice screen for the payer when opened with ?pay Sep 2, 2026
Clearing the callback on the live form values left two gaps. An answer that
arrived inside the debounce window wrote the superseded callback back, because
nothing invalidated the request that was already in flight; and the recipient
was not part of the check at all, so in the typed-payee branch a payer could
continue with a route they had already replaced. The callback is now only set
when the values the request was sent with still match the form, and the
recipient joins the invoice number and the amount in the clearing effect.

The payee error is announced when it appears instead of only being reachable
through the description, and the generated label id no longer starts with a
colon, so it can be used as a selector.
…onfig

The repository has no Prettier check in CI for TypeScript, so the four files
this branch adds had drifted from `.prettierrc`. `navigation.hook.ts` is left
alone: it does not match the config on develop either, and reformatting it
would touch lines this branch never wrote.
A response that belongs to values the payer has since changed no longer writes
anything: the comparison that used to guard only the success branch now runs
ahead of the error branch and the catch as well, and the effect that clears the
callback clears a rendered payment error with it. The confirmation tick in the
editable branch gets the accessible name the display branch already had, so the
end-to-end test can ask for the role instead of a layout class.

The tests follow the same three dimensions the code does: the invoice number had
no case of its own, the loading state was invisible behind a mock, and the seven
new translation keys had no check that all three languages carry them.
@joshuakrueger-dfx

Copy link
Copy Markdown
Contributor Author

EN:
Two things this branch cannot do for itself: someone with write access needs to apply the ci label so the checks run on 11b2f99, and both open change requests are addressed on a head that no longer exists, so they need a fresh look.

DE:
Zwei Dinge kann dieser Branch nicht selbst erledigen: Jemand mit Schreibrechten muss das Label ci setzen, damit die Checks auf 11b2f99 laufen, und beide offenen Änderungswünsche sind auf einem Head erledigt, den es nicht mehr gibt — sie brauchen einen neuen Blick.

Details

Why the checks are skipping

The pull request is a draft, so PR CI, CodeQL, the review bot and the handbook job all record
skipping. A draft runs them only with the ci or ci:full label, and applying a label needs
write access — this account has read access only. Marking it ready does not help either: the
head lives in a fork, and ci-on-ready cannot dispatch a fork head.

Separately, and not a label question: the full-stack stage takes mode=none on this pull request
no matter what. e2e-stack.yml probes E2E_API_CHECKOUT_KEY before it looks at the label, and a
fork pull request never receives repository secrets. Running it needs a branch inside this
repository or a manual workflow_dispatch.

What changed since the last review

30d47b89 was the last reviewed head. Since then:

  • The branch is rebased onto current develop (b3052bd) instead of carrying a merge commit.
    The seventeen reviewed commits are unchanged in content — git range-diff reports all of them
    as identical and the diff has the same patch id.
  • A payment answer can no longer outlive the values it was built from. Editing an already
    validated amount, invoice number or recipient used to leave the continue button active with the
    previous callback, so a payer could send values the screen no longer showed. Changing any of the
    three now clears the callback, and an answer may only write once the values it was sent with
    still match the form — on the failure paths as well, so a stale error neither appears nor
    lingers. The generation counter follows the pattern sell.screen.tsx already uses.
  • The displayed payee announces its error state (aria-invalid, aria-describedby, and the
    explanation announces itself) instead of carrying it in the text colour alone; its label id
    comes from useId() so a second mounted instance cannot collide with it; and the confirmation
    tick in the editable branch got the accessible name the display branch already had.
  • A fifth handbook baseline covers payer mode with a typed and confirmed payee — the one
    visual variant this change introduces that had no image.
  • Test and hygiene gaps: the invoice number was the only form dimension without a case of its
    own, two assertions were weaker than their titles, the seven new translation keys had no check
    that all three languages carry them, and four files this branch adds had drifted from
    .prettierrc (CI runs no Prettier check for TypeScript).

Two further defects are newly reported, not fixed — the confirmation tick proves the route
exists but not who owns it, and in payer mode a foreign query survives for the rest of the
session. Both are in the description; they need an exception granted or refused, like the five
before them.

Measured on 11b2f99

npm run lint exits with empty output, format:md:check and the repository Prettier config are
clean, build:dev and widget:dev pass, and the full Jest suite is 1881 of 1882 green — the
one failure is realunit.screen.test.tsx, outside this diff, and it fails identically on
unmodified develop. Coverage on the three instrumented files this pull request touches is
100 / 100 / 100 / 100; on develop all three sit at 0. The five Playwright baselines
regenerate and re-verify green, and all five are free of scannable codes and readable secrets
(zbarimg plus Apple Vision over the whole image).

@joshuakrueger-dfx

Copy link
Copy Markdown
Contributor Author

@marassteiner please re-review — head 11b2f99.

EN:
Your finding on docs/test-architecture.md is fixed, the branch is rebased onto current develop instead of carrying a merge commit, and four later commits close what my own review passes found — but no check has run on this head, because a draft from a fork needs the ci label and this account cannot apply one.

DE:
Dein Fund zu docs/test-architecture.md ist behoben, der Branch ist auf das aktuelle develop rebased statt über einen Merge-Commit integriert, und vier spätere Commits schliessen, was eigene Review-Pässe gefunden haben — auf diesem Head ist allerdings kein Check gelaufen, weil ein Draft aus einem Fork das Label ci braucht und dieser Account keines setzen kann.

Details

Since 30d47b89

  • Rebased onto develop (b3052bd); the seventeen reviewed commits are identical in content
    (git range-diff, same patch id), the merge commit is gone.
  • A payment answer can no longer outlive the values it was built from. Editing an already
    validated amount, invoice number or recipient left the continue button active with the previous
    callback, so a payer could send values the screen no longer showed. Changing any of the three
    clears the callback now, and an answer may only write once the values it was sent with still
    match the form — on the failure paths too. The generation counter follows the pattern
    sell.screen.tsx already uses.
  • The displayed payee announces its error state (aria-invalid, aria-describedby, and the
    explanation announces itself); the label id comes from useId(); the tick in the editable branch
    got the accessible name the display branch already had.
  • A fifth handbook baseline for payer mode with a typed and confirmed payee.
  • Gaps closed: the invoice number had no test case of its own, two assertions were weaker than
    their titles, the seven new translation keys had no language check, and four added files had
    drifted from .prettierrc.

Two defects are newly reported and not fixed — the tick proves the route exists but not who
owns it, and in payer mode a foreign query survives for the rest of the session. Both are in the
description and need an exception granted or refused, like the five before them.

Open, and not closable from this branch

  1. The ci label, so the checks run at all on 11b2f99.
  2. The full-stack stage, which takes mode=none regardless of the label: e2e-stack.yml probes
    E2E_API_CHECKOUT_KEY before it reads the label, and a fork pull request receives no repository
    secrets.
  3. TaprootFreak's change request of 2026-08-13 (payer flow in the full-stack harness) — the three
    payer cases have been in e2e-stack/specs/payment-links.spec.ts since f472ef15, but that
    review has not been revisited.

Measured on 11b2f99

Lint empty, format:md:check and the repository Prettier config clean, build:dev and
widget:dev green, Jest 1881 of 1882 passing — the single failure is realunit.screen.test.tsx
outside this diff, failing identically on unmodified develop. Coverage on the three instrumented
files: 100 / 100 / 100 / 100 (on develop all three are 0). Five Playwright baselines
regenerate and re-verify green, all five free of scannable codes and readable secrets.

@marassteiner

Copy link
Copy Markdown
Collaborator

EN: Working on this now — job dfxswiss__app__1274__pr-review__r3.
DE: Ich arbeite jetzt daran — Auftrag dfxswiss__app__1274__pr-review__r3.

@marassteiner

Copy link
Copy Markdown
Collaborator

codex-pr / quality (codex) rejected at 11b2f998f0707a86ece0fba6f872b4929701ef56

Codex vendor cannot run on this instance — not a code defect.

agent lane run --role pr-reviewer-quality --vendor codex returned STATUS=unavailable rc=1.
codex login status: Not logged in. CODEX_HOME is unset.
Direct codex exec: HTTP 401 Unauthorized (Missing bearer or basic authentication in header) on wss://api.openai.com/v1/responses and https://api.openai.com/v1/responses. Empty/unavailable output is not a complete pass.

Per the review contract: if a vendor cannot run, abort; do not record approved; do not substitute another vendor.

Grok quality on this head 11b2f998 (merge-base b3052bd): STATUS complete, approved. Prior unique-antecedent finding in docs/test-architecture.md is fixed (Both collection-invoice specs also fabricate the invoice rejection). Reality declaration for the invoice-screen recipient stub is present. Handbook: five baselines plus scripts/handbook/metadata.json invoice-screen. Seven payer keys in de/fr/it. Coverage claims 100/100/100/100 on the three instrumented files.

Written exceptions granted on this review:

Task stays in pr-review, not done. CI on this head is SKIPPED (draft from a fork, no ci label). Do not mark ready. Re-run when Codex is authenticated.

@marassteiner

Copy link
Copy Markdown
Collaborator

codex-pr / logic (codex) rejected at 11b2f998f0707a86ece0fba6f872b4929701ef56

Codex vendor cannot run on this instance — not a code defect.

agent lane run --role pr-reviewer-logic --vendor codex returned STATUS=unavailable rc=1.
codex login status: Not logged in. CODEX_HOME is unset.
Direct codex exec: HTTP 401 Unauthorized (Missing bearer or basic authentication in header) on wss://api.openai.com/v1/responses and https://api.openai.com/v1/responses. Empty/unavailable output is not a complete pass.

Per the review contract: if a vendor cannot run, abort; do not record approved; do not substitute another vendor.

Grok logic on this head 11b2f998 (merge-base b3052bd): STATUS complete, approved. Payer mode is an allowlist (1/true/yes). Query kept in payer mode, cleared in merchant mode. Continue uses object navigate plus replaceParams: true and only the validated payment search — lightning / recipient / pay do not inherit into /pl. Printed payee is display-only. First paint shows the URL payee. Payment answers cannot outlive the form (live clear, generation counter, isStaleForm on success and failure). Displayed payee announces its error state.

Written exceptions granted on this review:

Task stays in pr-review, not done. CI on this head is SKIPPED. Do not mark ready. Re-run when Codex is authenticated.

The screen test mocks `useNavigation` and can therefore only observe the intent;
the hook test drives the real hook without the screen. Their composition — the
one that carries the security claim of this branch — was covered by neither, and
no end-to-end test presses "Continue to payment" either. This one starts on a
printed payer URL that also carries `lightning` and `merchant`, clicks through to
the payment page and asserts the exact key set that arrives there.
`StyledInput` renders its visible label without `htmlFor` and the input without
an id, so the only accessible name a field has comes from its placeholder. Payer
mode deliberately drops the placeholder, which left a screen reader with an
unnamed textbox on the one screen written for that user. The field now carries
its label as an accessible name.

The test mock used to invent the label association the real component does not
make, which hid this and let the surrounding tests find fields by a name that
only existed in the mock. It now mirrors the component, and the locators follow
the same mechanism the application uses.

Also closes the recipient arm of the stale-answer guarantee: removing the
recipient from the comparison used to leave every test green.
@joshuakrueger-dfx

Copy link
Copy Markdown
Contributor Author

@marassteiner please re-review — head d2346d3.

EN:
Your Codex lane could not run on your instance, so I ran one here as an independent second family; it found two things that are now fixed — the payer's payee field had no accessible name, and the recipient arm of the stale-answer guarantee was untested — and the ci label is still the one thing standing between this head and a single green check.

DE:
Deine Codex-Lane konnte auf deiner Instanz nicht laufen, deshalb habe ich hier eine als unabhängige zweite Modellfamilie gefahren; sie fand zwei Punkte, die jetzt behoben sind — das Empfängerfeld im Zahler-Modus hatte keinen zugänglichen Namen, und der Empfänger-Arm der Stale-Answer-Zusage war ungetestet — und das Label ci ist weiterhin das Einzige, was zwischen diesem Head und einem einzigen grünen Check steht.

Details

What the second family found, and what came of it

  • The payer's payee field had no accessible name. StyledInput renders its visible label
    without htmlFor and the input without an id, so a field is named by its placeholder — and payer
    mode deliberately drops that placeholder. A screen reader met an unnamed textbox on the one
    screen written for that user. Verified against the installed component, not assumed. The field
    now carries its label as an accessible name; removing that attribute turns seven tests red.
  • The recipient arm of the stale-answer guarantee was untested. Measured: removing the
    recipient from the value comparison left all tests green. The existing recipient test changed the
    name only after the answer had arrived, so it exercised the clearing effect rather than the
    rejection of a delayed answer. The missing case is in, and the same mutation now turns it red.
  • On the way there, the test mock that invented a label association the real component does not
    make was replaced by one that mirrors the component, and the locators of the surrounding tests
    now use the same mechanism the application does.

Three of its other findings were already known and reported in the description (fetchJson
ignoring the HTTP status, the SDK rule, the validateRecipient race). One is newly reported and
not fixed: every recipient lookup failure — timeout, offline, 401, 500 — reads as "recipient not
recognized". It is pre-existing and now stands in the description with the others.

Thank you for granting the two new exceptions on 11b2f99; both are marked as granted in the
description.

Measured on d2346d3

Lint empty, Prettier clean on every file this branch adds, build:dev (the effective type check)
and widget:dev green, Jest 1884 of 1885 passing — the single failure is
realunit.screen.test.tsx, outside this diff, failing identically on unmodified develop.
Coverage on the three instrumented files: 100 / 100 / 100 / 100; on develop all three are 0.
Playwright 5/5 with all five baselines free of scannable codes and readable secrets.

Still not closable from this branch

  1. The ci label, so any check runs on this head at all.
  2. The full-stack stage, which takes mode=none regardless of the label: e2e-stack.yml probes
    E2E_API_CHECKOUT_KEY before it reads the label, and a fork pull request receives no repository
    secrets.
  3. TaprootFreak's change request of 2026-08-13 — the three payer cases it asked for have been in
    e2e-stack/specs/payment-links.spec.ts since then, but that review has not been revisited.

@marassteiner

marassteiner commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Job dfxswiss__app__1274__pr-review__r4: Der Lauf ist mit Fehlercode 1 beendet worden; es wurde nichts veröffentlicht.

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.

3 participants