Skip to content

fix(pos-app): cancel payment when leaving scan screen via back - #609

Merged
ignaciosantise merged 4 commits into
mainfrom
fix/pos-back-cancels-payment
Sep 1, 2026
Merged

fix(pos-app): cancel payment when leaving scan screen via back#609
ignaciosantise merged 4 commits into
mainfrom
fix/pos-back-cancels-payment

Conversation

@ignaciosantise

Copy link
Copy Markdown
Collaborator

Summary

Pressing back on the POS scan screen previously left the payment open at the gateway — only the on-screen Cancel button cancelled it. This wires the cancel logic into a single beforeRemove navigation listener so the hardware back button, header back arrow, and swipe-back gesture all cancel the payment uniformly.

Success and failure navigations are guarded by hasNavigatedRef (set before routing), so reaching a terminal state never triggers a cancel. The Android hardware back button is now also blocked while the header back and gesture are hidden (e.g. during processing). Also bumps Android versionCode 2 → 3.

Flow

flowchart TD
    A[User on scan screen] --> B{How do they leave?}
    B -->|Hardware back / header back / swipe| C[beforeRemove listener]
    B -->|Cancel button| D[resetNavigation] --> C
    B -->|Payment succeeds/fails| E[hasNavigatedRef = true] --> F[Navigate to result] --> C
    C --> G{hasNavigatedRef or already cancelled?}
    G -->|Yes| H[Skip - no cancel]
    G -->|No| I{status undefined or requires_action?}
    I -->|Yes| J[cancelPayment]
    I -->|No| H
Loading

Wire the scan screen's cancel logic into a `beforeRemove` navigation
listener so the hardware back button, header back arrow and swipe-back
gesture all cancel the open payment, not just the on-screen Cancel
button. Guard with `hasNavigatedRef` so success/failure navigations
never cancel, and block the Android hardware back while the header back
and gesture are hidden. Bump android versionCode 2 -> 3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Actions Updated
pos-demo Ignored Ignored Preview Sep 1, 2026 1:54pm UTC
react-native-appkit Ignored Ignored Preview Sep 1, 2026 1:54pm UTC
react-native-walletkit Ignored Ignored Preview Sep 1, 2026 1:54pm UTC

Request Review

Copilot AI lite review requested due to automatic review settings August 28, 2026 19:18
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown

Claude finished @ignaciosantise's task in 2m 38s —— View job


Review checklist

  • Gather context — read changed files
  • Analyze diff and full file
  • Run breaking-changes subagent
  • Post findings

Found 2 issue(s)

Issue 1: beforeRemove listener torn down and re-registered on every status poll

ID: scan-beforeremove-listener-churn-a3f1
File: dapps/pos-app/app/scan.tsx:228
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: cancelPendingPayment is recreated via useCallback every time paymentStatusData?.status changes (i.e. on every poll tick). The useEffect at line 228 lists it as a dep, so the beforeRemove listener is removed and re-added each poll cycle.
  • Risk: The listener has no actual gap in React's synchronous commit model, but it churns unnecessarily and couples the listener lifetime to the polling interval. If the polling dependency list grows or the effect order shifts, a real gap could appear.
  • Impact: Unnecessary listener churn; harder to reason about lifecycle correctness.
  • Trigger: Every successful status poll while the scan screen is mounted.

Recommendation: Use a ref to hold the latest callback and keep the listener stable:

const cancelPendingPaymentRef = useRef(cancelPendingPayment);
useEffect(() => { cancelPendingPaymentRef.current = cancelPendingPayment; }, [cancelPendingPayment]);

useEffect(() => {
  return navigation.addListener("beforeRemove", () => cancelPendingPaymentRef.current());
}, [navigation]); // stable — only re-registers if navigation object changes

Issue 2: "Try again" error toast shown after screen is already removed

ID: scan-cancel-toast-stale-context-b82c
File: dapps/pos-app/app/scan.tsx:219
Severity: LOW
Category: code_quality

Context:

  • Pattern: cancelPayment(paymentId).catch(...) is fire-and-forget inside beforeRemove. By the time the promise rejects, the navigation has already completed and the user is on a different screen.
  • Risk: showErrorToast("We couldn't cancel this payment. Try again.") appears over a screen unrelated to the failed cancel, and there is no action the user can take to retry from that screen.
  • Impact: Confusing UX — "Try again" implies an affordance that no longer exists.
  • Trigger: Network error or server error during cancelPayment when back navigation is used.

Recommendation: Replace with a message that doesn't imply a retry action, e.g. "Payment cancellation failed — contact support if you were charged.", or suppress the toast entirely and log server-side only.

No breaking changes detected (additive hook parameter with backward-compatible default; versionCode bump is internal).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR ensures any in-progress POS payment is consistently cancelled at the gateway when the user leaves the scan screen via navigation back actions (hardware back, header back, or swipe-back), instead of only via the on-screen Cancel button. It centralizes the cancel-on-leave behavior and prevents unintended cancels during success/failure routing.

Changes:

  • Add a single beforeRemove navigation listener on the scan screen to cancel pending payments on any “leave screen” action (with guards to avoid cancelling on terminal navigation).
  • Add an enabled flag to useDisableBackButton and use it on the scan screen to block Android hardware back when back/gesture UI is hidden.
  • Bump Android versionCode from 2 → 3.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
dapps/pos-app/hooks/use-disable-back-button.ts Adds an enabled parameter so hardware-back blocking can be toggled by screen state.
dapps/pos-app/app/scan.tsx Centralizes cancel-on-leave via beforeRemove, guards terminal navigations, and blocks Android hardware back when back UI is hidden.
dapps/pos-app/app.json Increments Android versionCode to reflect the release change.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Hold cancelPendingPayment in a ref so the beforeRemove listener registers
once for the screen's lifetime instead of re-registering on every status
poll. Drop "Try again" from the cancel-failure toast since the user has
already left the scan screen by the time it rejects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ignaciosantise

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown

Claude finished @ignaciosantise's task in 2m 18s —— View job


Review checklist

  • Gather context — read changed files
  • Analyze diff and full files
  • Post findings

🔄 Incremental Review (continuing from prior auto-review)

Both previously flagged issues are resolved in the current commits:

  • scan-beforeremove-listener-churn-a3f1 — Fixed. The ref pattern (cancelPendingPaymentRef) now keeps the beforeRemove listener stable across status polls; only re-registered if navigation changes.
  • scan-cancel-toast-stale-context-b82c — Fixed. Error message no longer says "Try again." since there's no retry affordance on the destination screen.

The useDisableBackButton(enabled) change is clean and backward-compatible (default true matches prior callers). The backHidden derivation correctly maps payment status to the enabled flag.

Found 1 issue(s)

Issue 1: Payment opened at gateway not cancelled when user backs out during in-flight startPayment

ID: scan-inflight-startpayment-leak-c91a
File: dapps/pos-app/app/scan.tsx:216
Severity: LOW
Category: code_quality

Context:

  • Pattern: cancelPendingPayment guards on paymentId state (if (paymentId && ...)). If the user navigates back while startPayment is still awaiting (i.e., before setPaymentId fires), paymentId is null, the guard is false, and no cancel is attempted. When startPayment later resolves it sets state on an unmounted component — the payment is open at the gateway with no cleanup path.
  • Risk: Orphaned payment at the gateway for the narrow window between startPayment() resolving and React re-rendering with the new paymentId.
  • Impact: Gateway holds a dangling open payment; pre-existing before this PR but relevant since this PR is specifically about closing the cancel gap.
  • Trigger: User presses back during the initial network call to create a payment (slow connection scenario).

Recommendation: Track the in-flight payment ID outside React state so cancelPendingPayment can reach it before the first render with the new paymentId:

const pendingPaymentIdRef = useRef<string | null>(null);

// Inside initiatePayment, after startPayment resolves:
pendingPaymentIdRef.current = data.paymentId;
setPaymentId(data.paymentId);

// Inside cancelPendingPayment, use the ref as fallback:
const id = paymentId ?? pendingPaymentIdRef.current;
if (id && (status === undefined || status === "requires_action")) {
  hasCancelledRef.current = true;
  cancelPayment(id).catch(...);
}

Close the remaining leak: if the user leaves the scan screen while the
initial startPayment create call is still in flight, paymentId state
doesn't exist yet for the beforeRemove listener to cancel. Track the
leave in a ref and cancel the freshly opened payment when the call
resolves, skipping the now-unmounted state updates and terminal routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ignaciosantise

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Sep 1, 2026

Copy link
Copy Markdown

Claude finished @ignaciosantise's task in 1m 25s —— View job


Review checklist

  • Gather context — read changed files
  • Analyze diff and current file state
  • Check status of previously flagged issue scan-inflight-startpayment-leak-c91a
  • Post findings

🔄 Incremental Review (continuing from prior reviews)

Previously flagged issue is resolved in the current commits:

  • scan-inflight-startpayment-leak-c91a — Fixed. The hasLeftRef ref is set to true inside cancelPendingPayment before checking paymentId, and initiatePayment checks hasLeftRef.current immediately after startPayment resolves to cancel a freshly created payment if the user already navigated away. The race window is closed.

No new issues found in latest commits.

The hasLeftRef / hasCancelledRef / hasNavigatedRef tri-ref pattern is clean and correctly handles all three exit paths (in-flight create, cancel-on-back, and terminal-state routing) without double-cancel.

@ignaciosantise
ignaciosantise merged commit fd147fa into main Sep 1, 2026
11 checks passed
@ignaciosantise
ignaciosantise deleted the fix/pos-back-cancels-payment branch September 1, 2026 14:25
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.

2 participants