Skip to content

fix(checkout): decrypt-failure hardening (consolidates #43 + #45, closes #44) - #46

Open
gitchadd wants to merge 5 commits into
mainfrom
fix/decrypt-failure-hardening
Open

fix(checkout): decrypt-failure hardening (consolidates #43 + #45, closes #44)#46
gitchadd wants to merge 5 commits into
mainfrom
fix/decrypt-failure-hardening

Conversation

@gitchadd

Copy link
Copy Markdown
Collaborator

Consolidates the two coupled decrypt-failure PRs (supersedes #43 + #45) into one branch — the dual-agent review found a merge-ordering hazard between them, and the top follow-up (a shared sentinel constant) spans both files, so they're better as one PR. Applies all three review follow-ups.

What it does

  1. Throw guard (was fix(checkout): guard decrypt against identity-resolution throws #43): decryptAndDispatch wraps the decrypt in try/catch so a thrown resolveIdentity()/localStorage error no longer strands the accepted screen on "Decrypting…".
  2. Failure rendering (was fix(checkout): guard decrypt-failure sentinel + client-side INR QR (closes #44) #45): the failure sentinel renders "Couldn't load payment details. Please contact support." (any currency) instead of a copyable/QR-able fake VPA.
  3. Third-party leak closed (was fix(checkout): guard decrypt-failure sentinel + client-side INR QR (closes #44) #45): the api.qrserver.com <img> is replaced by a lazy client-side qrcode.react QR (counterparty VPA + amount + order id no longer sent to a third party).

Review follow-ups applied

  • Shared DECRYPT_FAILED_SENTINEL constant in order-machine.ts, used by the helper and Checkout.tsx — kills the cross-file "Session changed" magic string (verified: no stray literal outside the constant).
  • Restored the .includes("@") VPA guard on the INR QR (via showInrQr()) — closes the malformed-Result-Ok edge the port had dropped.
  • Render coverage: extracted pure paymentAddressView() / showInrQr() helpers + a unit test (test/checkoutAddressView.test.ts) covering the decrypt-failure branch and the @-guard — Checkout previously had no render harness.

Verification

Closes #44. Deploy gate: human review + explicit per-merge approval. Not for auto-merge.

gitchadd added 2 commits June 18, 2026 14:46
resolveIdentity() reads localStorage directly, which throws on corrupt identity
JSON or when storage is blocked (sandboxed / partitioned iframe). The poll-loop
and resume decrypt sites swallow throws in their own catch{}, so a throw left the
accepted screen stuck on "Decrypting..." with no recovery. Extract the shared
decrypt into decryptAndDispatch() wrapped in try/catch so any failure (Result-Err
or thrown) falls back to the existing "Session changed" sentinel.

Adds a regression test mocking the store to throw. verify green: 143 tests,
typecheck + build clean. Ported from the archived feat/upi-intent branch
(2f8ad58); the underlying issue exists independently on main's decrypt path.
…ared sentinel

Consolidates the decrypt-failure hardening (supersedes PRs #43 + #45) and applies
the dual-review follow-ups:
- Shared DECRYPT_FAILED_SENTINEL constant in order-machine, used by the helper and
  by Checkout, replacing the cross-file "Session changed" magic string.
- Checkout shows "Couldn't load payment details. Please contact support." for the
  failure sentinel (any currency) instead of rendering it as a copyable/QR-able VPA.
- INR QR gated via showInrQr() on a real "@" VPA (restores the dropped guard) + a
  known amount; replaces the third-party api.qrserver.com <img> with a lazy
  client-side qrcode.react QR, closing the VPA+amount data leak.
- Extracts pure paymentAddressView()/showInrQr() helpers + a unit test, giving the
  decrypt-failure branch real coverage (Checkout had no render harness).

Closes #44. verify green: typecheck + build + 150 tests; no api.qrserver.com in the
runtime bundle; sentinel centralized to one constant.
gitchadd added 2 commits June 18, 2026 17:40
Per the e2e design critique: render the decrypt-failure message in the existing
dangerSoft error box (matching state.error) with a warning icon, instead of bare
red text. Pure visual consistency on the error branch; no logic/flow change.
When the counterparty VPA can't be decrypted (addrView === "error") the user has
no payment details and can't have paid, so disable + dim the "I've paid" button;
the remaining actions are Contact support (per the message) and Cancel order.
@gitchadd

Copy link
Copy Markdown
Collaborator Author

Final agentic review (full PR, 4 commits)

Two strictly read-only agents (bug-hunter, compliance) over origin/main...HEAD, focused on the two newest commits (error-box polish 9fb5e55, disable-on-error c41c227), plus my own verification + the e2e.

Verdict: clean, shippable. No production bugs, compliance PASS.

  • Disable-on-error correct, no regression. "I've paid" is disabled ONLY when decryptedUpi === DECRYPT_FAILED_SENTINEL (addrView === "error"); happy/compound/address states are unaffected. Verified e2e — enabled on the QR screen, disabled (and dimmed) on the error screen.
  • Error-box polish clean. Valid inline SVG (correct React camelCase, aria-hidden decorative with text in the <span>); faithfully reuses the existing dangerSoft state.error box style; all tokens exist.
  • Older commits intact. Single shared sentinel constant (no literal drift), @-guard QR double-safe, client-side QR lazy-loaded (library internals not inlined).
  • Compliance PASS. The disable logic is unit-covered (paymentAddressView/decryptAndDispatch tests); only the button wiring + error markup are render-gated (the repo has no Checkout render harness) and were e2e-verified — the honest Directive-feat(v1.1.1-bridge): chatEnabled flag + actionable filter — temporary fallback #7 "untestable-here, e2e-verified" category. 150 tests accurate, no scope creep, no co-author trailer, vocab clean. Worktree clean before/after.

One pre-existing finding (NOT introduced; non-blocking)

During the transient "Decrypting…" state (decryptedUpi still null), "I've paid" is ENABLED — a user could mark-paid before any VPA renders. Unchanged from main (this PR added the disable only for the failure sentinel, as requested). Low severity (self-defeating; short decrypt window; merchant accept gates settlement). To be fixed-in-PR or filed per the owner's call.

Deploy gate: merge needs explicit per-merge approval. This review is the agentic step.

… failure)

Generalize the guard via the unit-tested payableAddressShown() helper: "I've paid"
is actionable only once a payable address (or compound fields) is on screen, so it
is disabled during both the transient "Decrypting..." state and a decrypt failure
— the user can't mark-paid before any VPA is shown. Closes the pre-existing
decrypting-state gap flagged in the final review.
@gitchadd

Copy link
Copy Markdown
Collaborator Author

Final-review finding closed. The one (pre-existing) finding — "I've paid" enabled during the transient "Decrypting…" state — is now fixed in e19eaa2: the button is disabled whenever a payable address isn't on screen (decrypt failure OR decrypting), via the unit-tested payableAddressShown() helper. verify green: typecheck + build + 152 tests. e2e confirms "I've paid" is disabled during "Decrypting…" (screenshot captured). No open findings remain.

gitchadd added a commit that referenced this pull request Jun 18, 2026
gitchadd added a commit that referenced this pull request Jun 18, 2026
gitchadd added a commit that referenced this pull request Jun 18, 2026
gitchadd added a commit that referenced this pull request Jun 18, 2026
@gitchadd

Copy link
Copy Markdown
Collaborator Author

E2E screenshots — PR #46 (decrypt-failure hardening)

Captured from a real-browser run against the demo built on this branch (retina). "I've paid" button state called out per screen.

1. Checkout

checkout

2. Accepted — client-side QR (replaces api.qrserver.com; "I've paid" enabled)

accepted

3. Decrypt failure — boxed error, no fake VPA/QR ("I've paid" disabled)

error

4. Decrypting… ("I've paid" disabled)

decrypting

Images hosted on the pr46-screenshots branch (off the PR diff); deletable after merge.

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.

Decrypt-failure sentinel "Session changed" rendered as a VPA (copy-row + INR QR), no @-guard

1 participant