Skip to content

fix: ensure Checkout.open() promise always resolves/rejects after FPX flow - #102

Open
rzp-slash[bot] wants to merge 1 commit into
masterfrom
fix/fpx-promise-not-resolving
Open

rzp-slash[bot] wants to merge 1 commit into
masterfrom
fix/fpx-promise-not-resolving

Conversation

@rzp-slash

@rzp-slash rzp-slash Bot commented Aug 4, 2026

Copy link
Copy Markdown

Problem

A merchant reported that Checkout.open() does not consistently resolve or reject after an FPX (Malaysia) payment flow completes — especially for failed or cancelled transactions. The merchant app never regains control to verify the payment or update the UI. Cards, eWallets, and other methods work correctly.

Root Cause

Android (primary bug)

In Checkout.java, onPaymentError() called:

lastSavedCall.reject(paymentData.getData().getJSONObject("error").toString(), ...)

For FPX redirect failures/cancellations, the native SDK returns a PaymentData object whose JSON does not contain an "error" key (or returns null data). This throws a JSONException, which is caught by the catch block — but the catch block only called printStackTrace() and never called lastSavedCall.reject(). The promise stayed pending forever.

This is why it's FPX-specific: FPX is a redirect-based method where the error/failure path differs from synchronous methods like cards. The native SDK's error payload structure differs, and the wrapper assumed it would always have an error object.

iOS (secondary issue)

The self.call reference was never cleared after resolve()/reject(). For redirect-based methods like FPX, the delegate may fire more than once (e.g., an intermediate error followed by a final callback), leading to double-callback issues where the second invocation finds call already resolved/rejected.

Web (edge case)

No error event listener on the dynamically injected checkout.js script tag — if the script failed to load, the promise would hang forever.

Fix

Android (Checkout.java)

  • onPaymentError() now defensively parses paymentData.getData() with null checks and a fallback error JSON ({code, description})
  • Always calls lastSavedCall.reject() — even in the catch path, a reject is guaranteed with a fallback error string
  • Added null check for lastSavedCall (matching the existing pattern in onPaymentSuccess)

iOS (Plugin.swift)

  • Set self.call = nil after both resolve() and reject() to prevent double-callback issues with redirect-based payment methods

Web (web.ts)

  • Added rzpjs.addEventListener('error', ...) to reject the promise if checkout.js fails to load
  • Applied prettier formatting

Testing

  • TypeScript compiles cleanly (tsc --noEmit + npm run build pass)
  • Prettier check passes
  • The fix is backward-compatible — success path is unchanged; only the error/failure path is hardened

Merchant Impact

This resolves the merchant's core issue: Checkout.open() will now always resolve or reject after the FPX flow completes, regardless of success/failure/cancellation — consistent with cards, eWallets, and other methods.

… flow

Root cause: Android onPaymentError silently swallowed JSONException when
the 'error' key was missing from paymentData (common for FPX redirect
failures/cancellations). The catch block only called printStackTrace()
without ever calling lastSavedCall.reject(), leaving the promise pending
forever — the merchant app never regained control.

Changes:
- Android: onPaymentError now defensively parses error data with null
  checks and fallback error JSON, always calls lastSavedCall.reject()
  even in the catch path. Added null check for lastSavedCall.
- iOS: Clear self.call after resolve/reject to prevent double-callback
  issues with redirect-based methods like FPX where the delegate may
  fire more than once.
- Web: Add script 'error' event listener so the promise rejects if
  checkout.js fails to load instead of hanging forever.
- Apply prettier formatting to src files.

Co-authored-by: tanishq-rzpayyy <tanishq.birania@razorpay.com>
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.

1 participant