Skip to content

feat: Implement order validation and payment confirmation modal - #607

Open
sundayonah wants to merge 4 commits into
mainfrom
feat/noblocks-stuck-payment-confirmation-ui
Open

feat: Implement order validation and payment confirmation modal#607
sundayonah wants to merge 4 commits into
mainfrom
feat/noblocks-stuck-payment-confirmation-ui

Conversation

@sundayonah

@sundayonah sundayonah commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR adds a stuck-transaction prompt so users can manually confirm receipt when a swap stays in Fulfilling or Fulfilled for more than 2 minutes. It reduces support load and lets users settle the transaction without waiting for the provider to update.

Background: Swaps can remain in "fulfilling" or "fulfilled" for longer than expected. The user may have already received funds. This flow asks "Have you received this payment?" and, on confirmation, validates the order on the aggregator and marks the Noblocks transaction as settled.

Changes:

  • Stuck detection: Modal appears only when the transaction has been in fulfilling or fulfilled for 120 seconds (time in that state, not since transaction creation).
  • Persistence: "Stuck since" is stored in localStorage keyed by orderId, so the 120s count survives refresh and navigating away. On open/refresh, if already stuck ≥120s, the prompt can show immediately.
  • Confirm (Yes): User taps Yes, Payment Received → Noblocks calls POST /api/v1/orders/:id/validate (proxy to aggregator sender API) → aggregator validates order → Noblocks updates transaction to settled/completed and dismisses the prompt. Stuck timestamp is cleared from localStorage. No, I haven't dismisses without validating.
  • UI: Redesigned PaymentConfirmationModal to match Figma (Pending badge, amount + destination address + View, Yes/No actions).
  • Config: Uses config.aggregatorUrl and config.aggregatorSenderApiKeyId from @/app/lib/config for the validate route (falls back to NEXT_PUBLIC_AGGREGATOR_SENDER_API_KEY_ID when the server-only key is unset).
  • Scope: Prompt only for fulfilling and fulfilled (not refunding), per product spec.

New/updated surface:

  • Noblocks: POST /api/v1/orders/[id]/validate (rate-limited, requires x-wallet-address), validateOrder() in aggregator.ts, TransactionStatus 120s + localStorage logic and handlePaymentConfirmed, redesigned PaymentConfirmationModal.
  • Config: aggregatorSenderApiKeyId added to Config and config.ts; env AGGREGATOR_SENDER_API_KEY_ID (UUID) preferred for validate.
  • No breaking changes. Existing flows unchanged when the prompt is not shown or not used.

References

Screenshot 2026-07-20 180001

Checklist

  • I have added documentation and tests for new/changed functionality in this PR
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main

By submitting a PR, I agree to Paycrest's Contributor Code of Conduct and Contribution Guide.

Summary by CodeRabbit

  • New Features

    • Added a payment confirmation modal for on-ramp/off-ramp transactions that remain pending beyond a delay.
    • Added “stuck payment” session persistence so the confirmation flow can resume after refresh and re-authentication.
  • Improvements

    • Improved backend order validation by adding a dedicated, rate-limited validation endpoint with clearer success/failure handling.
    • Enhanced transaction polling and reindex targeting to better determine the correct transaction hash/network for retries.
  • Chores

    • Improved session cleanup to remove stuck-payment related local storage data on logout/reset.

- Added `validateOrder` function to confirm user receipt of funds for stuck orders, integrating with the aggregator's validate API.
- Introduced `PaymentConfirmationModal` component to prompt users for payment confirmation after a delay if their transaction is stuck.
- Updated `TransactionStatus` to manage payment confirmation state and handle user interactions for confirming payments.
- Enhanced configuration to include `aggregatorSenderApiKeyId` for secure API interactions.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bab9cfb-d07e-47a1-aa01-3da46a5f3e08

📥 Commits

Reviewing files that changed from the base of the PR and between f56e9a4 and f6cf871.

📒 Files selected for processing (3)
  • app/components/PaymentConfirmationModal.tsx
  • app/lib/stuckPaymentSession.ts
  • app/pages/TransactionStatus.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/lib/stuckPaymentSession.ts
  • app/components/PaymentConfirmationModal.tsx
  • app/pages/TransactionStatus.tsx

📝 Walkthrough

Walkthrough

Adds authenticated order validation through a server proxy and introduces persistent delayed payment confirmation for stuck transactions. Confirmed payments are validated and settled; declined payments are reindexed and tracked again.

Changes

Payment confirmation flow

Layer / File(s) Summary
Stuck-payment persistence and restoration
app/lib/stuckPaymentSession.ts, app/lib/session-cleanup.ts, app/components/MainPageContent.tsx
Persists stuck payment form and timer state, restores it after authentication, and clears it during logout or reset.
Payment confirmation modal
app/components/PaymentConfirmationModal.tsx, app/components/index.ts
Adds a busy-state confirmation dialog with confirm and decline actions, payment details, recipient information, and a public export.
Authenticated order validation proxy
app/types.ts, app/lib/config.ts, app/api/v1/orders/[id]/validate/route.ts, app/api/aggregator.ts
Adds sender API-key configuration, wallet-authorized validation routing, downstream timeout/error handling, analytics, and a client validation helper.
Stuck transaction confirmation integration
app/lib/reindex.ts, app/pages/TransactionStatus.tsx
Tracks fulfilling or fulfilled transactions for 120 seconds, validates confirmed orders before settling them, and reindexes declined payments while restarting tracking.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TransactionStatus
  participant PaymentConfirmationModal
  participant validateOrder
  participant POSTValidateRoute
  participant SupabaseTransactions
  participant SenderValidationService
  TransactionStatus->>PaymentConfirmationModal: Open after 120 seconds
  PaymentConfirmationModal->>TransactionStatus: Confirm payment
  TransactionStatus->>validateOrder: Validate order
  validateOrder->>POSTValidateRoute: POST authenticated request
  POSTValidateRoute->>SupabaseTransactions: Check wallet ownership
  POSTValidateRoute->>SenderValidationService: Validate order
  SenderValidationService-->>POSTValidateRoute: Return validation result
  POSTValidateRoute-->>validateOrder: Return validation result
  validateOrder-->>TransactionStatus: Settle validated transaction
  TransactionStatus-->>PaymentConfirmationModal: Close modal
Loading

Possibly related PRs

Suggested reviewers: onahprosper

Poem

A rabbit found a payment still,
And gave it time beside the hill.
Confirmed, it settled bright and neat;
Declined, it hopped to check the beat.
“Good hops!” sings Bunny, quick and sweet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: order validation plus the payment confirmation modal.
Description check ✅ Passed The PR description covers the change, references, and checklist, though it omits a dedicated Testing section and environment details.
Linked Issues check ✅ Passed The changes implement the stuck-state prompt, confirmation settlement path, and reindex/retry flow required by issue #354.
Out of Scope Changes check ✅ Passed The additional files and logic all support the stuck-transaction confirmation flow and do not appear unrelated to the stated objective.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

Dprof-in-tech
Dprof-in-tech previously approved these changes Jul 20, 2026

@Dprof-in-tech Dprof-in-tech 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.

LGTM!

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/pages/TransactionStatus.tsx (1)

1725-1744: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

recipientAddress shows a bank/mobile-money identifier as if it were a destination wallet for off-ramp stuck orders.

recipientWalletAddress is sourced from FormData.walletAddress, documented elsewhere as onramp-only ("For onramp: stablecoin wallet address"). For off-ramp swaps — the case where fulfilling/fulfilled most commonly stalls (PSP delay paying out fiat) — recipientWalletAddress is empty, so String(accountIdentifier || "") (a bank account/mobile-money number) is passed into recipientAddress, which PaymentConfirmationModal documents and renders as "Destination wallet where funds are going" via truncateAddress's hex-style formatting. This misrepresents where the funds actually went during the exact moment the user is deciding whether to confirm receipt.

Pass a swap-direction-aware label (or omit recipientAddress for off-ramp and rely on the token/amount + explorer link) instead of reusing the wallet-address prop for a non-wallet identifier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/TransactionStatus.tsx` around lines 1725 - 1744, Update the
PaymentConfirmationModal invocation in TransactionStatus so off-ramp swaps do
not pass accountIdentifier as recipientAddress; omit the prop or provide a
direction-appropriate label. Preserve recipientWalletAddress for onramp flows,
ensuring the modal’s destination-wallet display never formats bank or
mobile-money identifiers as wallet addresses.
🧹 Nitpick comments (3)
app/pages/TransactionStatus.tsx (1)

891-923: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Effect re-runs on every poll tick due to full orderDetails object in deps.

orderDetails gets a new reference every ~5s from setOrderDetails(responseData) in the polling effect, and it's in this effect's dependency array, so writeStuckPaymentSession re-serializes to localStorage and the 120s timer is torn down/rebuilt every poll cycle while a transaction is stuck (potentially for a long time). Functionally harmless (the elapsed-time math is ref-based and stays correct), but it's needless localStorage churn and timer thrash.

Consider depending only on the specific fields you actually use (orderDetails?.network, orderDetails?.txHash) instead of the whole object.

Also applies to: 954-971

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/TransactionStatus.tsx` around lines 891 - 923, Update the effect
containing writeStuckPaymentSession and its related 120-second timer to remove
the full orderDetails object from the dependency array. Depend only on the
specific values read by the effect, orderDetails?.network and
orderDetails?.txHash, while preserving the existing session persistence
behavior.
app/lib/stuckPaymentSession.ts (2)

3-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Session key and per-order timer prefix are hardcoded independently in three files instead of being centralized. SESSION_KEY ("noblocks_stuck_payment_session") and the stuck_fulfilling_since_ prefix live only as private constants in stuckPaymentSession.ts, so every other consumer re-types the literal strings (or, in one case, re-implements logic the module already exports) — any future rename silently breaks the others.

  • app/lib/stuckPaymentSession.ts#L3-L102: export SESSION_KEY and the stuck_fulfilling_since_ prefix (or a getStuckFulfillingSinceKey(orderId) helper) so other modules can import them instead of duplicating the literals.
  • app/lib/session-cleanup.ts#L5-L49: replace the hardcoded "noblocks_stuck_payment_session" (line 11) with a call to clearStuckPaymentSession(), and replace the hardcoded "stuck_fulfilling_since_" prefix scan (34-49) with the exported prefix constant.
  • app/pages/TransactionStatus.tsx#L858-L949: drop the locally hardcoded STUCK_STORAGE_KEY_PREFIX and call the already-imported clearStuckFulfillingSince(orderId) (871-878) and resetStuckFulfillingSince(orderId) (940-947) instead of re-implementing the same localStorage read/write logic inline.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/stuckPaymentSession.ts` around lines 3 - 102, Centralize the
stuck-session storage identifiers and timer operations: in
app/lib/stuckPaymentSession.ts lines 3-102, export SESSION_KEY and the
stuck_fulfilling_since_ prefix (or a key-building helper); in
app/lib/session-cleanup.ts lines 5-49, use clearStuckPaymentSession() and the
exported timer prefix instead of hardcoded literals; in
app/pages/TransactionStatus.tsx lines 858-949, remove STUCK_STORAGE_KEY_PREFIX
and replace inline localStorage timer access with
clearStuckFulfillingSince(orderId) and resetStuckFulfillingSince(orderId).

3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

SESSION_KEY and the stuck_fulfilling_since_ prefix are duplicated in other files.

Both this file's SESSION_KEY string and its stuck_fulfilling_since_ prefix are hard-coded again in app/lib/session-cleanup.ts and app/pages/TransactionStatus.tsx instead of being imported/reused. See the consolidated comment below.

Also applies to: 86-102

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/stuckPaymentSession.ts` at line 3, Centralize the shared SESSION_KEY
and stuck_fulfilling_since_ prefix in stuckPaymentSession.ts, exporting them for
reuse. Update session-cleanup.ts and TransactionStatus.tsx to import these
constants instead of duplicating the hard-coded strings, preserving existing
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/stuckPaymentSession.ts`:
- Around line 40-58: Update readStuckPaymentSession to reject otherwise-valid
sessions whose savedAt or createdAt timestamp exceeds a reasonable short TTL of
a few hours, using the available timestamp consistently and preserving null
returns for invalid or expired data. Ensure stale sessions are not restored by
the existing navigation flow.

In `@app/pages/TransactionStatus.tsx`:
- Around line 1038-1077: Update handlePaymentNotReceived so
setShowPaymentConfirmation(false) and the successful reindex timer-reset actions
run only after reindexSingleTransaction succeeds; preserve the modal’s open
state when validation or reindexing fails, while retaining the existing success
behavior and retry-related state updates.

---

Outside diff comments:
In `@app/pages/TransactionStatus.tsx`:
- Around line 1725-1744: Update the PaymentConfirmationModal invocation in
TransactionStatus so off-ramp swaps do not pass accountIdentifier as
recipientAddress; omit the prop or provide a direction-appropriate label.
Preserve recipientWalletAddress for onramp flows, ensuring the modal’s
destination-wallet display never formats bank or mobile-money identifiers as
wallet addresses.

---

Nitpick comments:
In `@app/lib/stuckPaymentSession.ts`:
- Around line 3-102: Centralize the stuck-session storage identifiers and timer
operations: in app/lib/stuckPaymentSession.ts lines 3-102, export SESSION_KEY
and the stuck_fulfilling_since_ prefix (or a key-building helper); in
app/lib/session-cleanup.ts lines 5-49, use clearStuckPaymentSession() and the
exported timer prefix instead of hardcoded literals; in
app/pages/TransactionStatus.tsx lines 858-949, remove STUCK_STORAGE_KEY_PREFIX
and replace inline localStorage timer access with
clearStuckFulfillingSince(orderId) and resetStuckFulfillingSince(orderId).
- Line 3: Centralize the shared SESSION_KEY and stuck_fulfilling_since_ prefix
in stuckPaymentSession.ts, exporting them for reuse. Update session-cleanup.ts
and TransactionStatus.tsx to import these constants instead of duplicating the
hard-coded strings, preserving existing behavior.

In `@app/pages/TransactionStatus.tsx`:
- Around line 891-923: Update the effect containing writeStuckPaymentSession and
its related 120-second timer to remove the full orderDetails object from the
dependency array. Depend only on the specific values read by the effect,
orderDetails?.network and orderDetails?.txHash, while preserving the existing
session persistence behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c82dcbfc-286f-4bea-9104-c551eff582cf

📥 Commits

Reviewing files that changed from the base of the PR and between cb6c520 and 775ff59.

📒 Files selected for processing (5)
  • app/components/MainPageContent.tsx
  • app/components/PaymentConfirmationModal.tsx
  • app/lib/session-cleanup.ts
  • app/lib/stuckPaymentSession.ts
  • app/pages/TransactionStatus.tsx

Comment thread app/lib/stuckPaymentSession.ts
Comment thread app/pages/TransactionStatus.tsx Outdated

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/components/PaymentConfirmationModal.tsx (1)

135-163: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Modal's recipient display assumes a wallet address; off-ramp passes a bank/mobile-money identifier instead. truncateAddress + monospace formatting is designed for on-chain hashes, but off-ramp orders (the primary target of this feature, per the off-ramp-only auto-reindex logic) feed in accountIdentifier, producing a truncated bank account number styled as a crypto address.

  • app/components/PaymentConfirmationModal.tsx#L135-L163: branch the recipient row's formatting on whether the value is a wallet address vs. an account identifier (e.g., skip truncateAddress/font-mono for non-address values).
  • app/pages/TransactionStatus.tsx#L1732-L1750: pass a type hint (or the institution name) alongside recipientAddress so the modal can render bank recipients appropriately instead of as a truncated hash.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/PaymentConfirmationModal.tsx` around lines 135 - 163, Update
PaymentConfirmationModal’s recipient row to distinguish wallet addresses from
off-ramp account identifiers, applying truncateAddress and font-mono only to
wallet values while displaying bank or mobile-money identifiers in full with
normal styling. Update the TransactionStatus payment confirmation call site to
pass the required recipient type hint or institution name so the modal can
select the appropriate rendering; apply these changes in
app/components/PaymentConfirmationModal.tsx lines 135-163 and
app/pages/TransactionStatus.tsx lines 1732-1750.
app/pages/TransactionStatus.tsx (1)

916-945: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Stuck-payment session should not persist recipient PII in localStorage.

writeStuckPaymentSession stores recipientName and accountIdentifier in a script-readable, persistent store. Keep only the fields needed to resume the flow, or move this data to shorter-lived storage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/TransactionStatus.tsx` around lines 916 - 945, Update the
writeStuckPaymentSession payload in TransactionStatus so it no longer persists
recipientName or accountIdentifier in localStorage-backed session data. Remove
these PII fields while preserving the remaining resume-flow fields and existing
writeStuckPaymentSession behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/pages/TransactionStatus.tsx`:
- Around line 1068-1071: Add a user-visible toast.error call in
handlePaymentConfirmed’s catch block before rethrowing the payment confirmation
error, while preserving the existing logging and propagation. In
app/pages/TransactionStatus.tsx lines 1068-1071, include clear failure feedback;
in app/components/PaymentConfirmationModal.tsx lines 48-58, no functional change
is required, but update the stale comment to reflect that the parent surfaces
errors via a toast.

---

Outside diff comments:
In `@app/components/PaymentConfirmationModal.tsx`:
- Around line 135-163: Update PaymentConfirmationModal’s recipient row to
distinguish wallet addresses from off-ramp account identifiers, applying
truncateAddress and font-mono only to wallet values while displaying bank or
mobile-money identifiers in full with normal styling. Update the
TransactionStatus payment confirmation call site to pass the required recipient
type hint or institution name so the modal can select the appropriate rendering;
apply these changes in app/components/PaymentConfirmationModal.tsx lines 135-163
and app/pages/TransactionStatus.tsx lines 1732-1750.

In `@app/pages/TransactionStatus.tsx`:
- Around line 916-945: Update the writeStuckPaymentSession payload in
TransactionStatus so it no longer persists recipientName or accountIdentifier in
localStorage-backed session data. Remove these PII fields while preserving the
remaining resume-flow fields and existing writeStuckPaymentSession behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e4c64c7-b86b-431c-a81b-48369162fddf

📥 Commits

Reviewing files that changed from the base of the PR and between 775ff59 and f56e9a4.

📒 Files selected for processing (3)
  • app/components/PaymentConfirmationModal.tsx
  • app/lib/reindex.ts
  • app/pages/TransactionStatus.tsx

Comment thread app/pages/TransactionStatus.tsx
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.

Implement Unfulfilled Transaction Confirmation Flow

2 participants