fix: ensure Checkout.open() promise always resolves/rejects after FPX flow - #102
Open
rzp-slash[bot] wants to merge 1 commit into
Open
rzp-slash[bot] wants to merge 1 commit into
rzp-slash[bot] wants to merge 1 commit into
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:For FPX redirect failures/cancellations, the native SDK returns a
PaymentDataobject whose JSON does not contain an "error" key (or returns null data). This throws aJSONException, which is caught by the catch block — but the catch block only calledprintStackTrace()and never calledlastSavedCall.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
errorobject.iOS (secondary issue)
The
self.callreference was never cleared afterresolve()/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 findscallalready resolved/rejected.Web (edge case)
No
errorevent listener on the dynamically injectedcheckout.jsscript tag — if the script failed to load, the promise would hang forever.Fix
Android (
Checkout.java)onPaymentError()now defensively parsespaymentData.getData()with null checks and a fallback error JSON ({code, description})lastSavedCall.reject()— even in the catch path, a reject is guaranteed with a fallback error stringlastSavedCall(matching the existing pattern inonPaymentSuccess)iOS (
Plugin.swift)self.call = nilafter bothresolve()andreject()to prevent double-callback issues with redirect-based payment methodsWeb (
web.ts)rzpjs.addEventListener('error', ...)to reject the promise if checkout.js fails to loadTesting
tsc --noEmit+npm run buildpass)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.