REACT NATIVE

Razorpay Sucess Rate Increase By 300%
| React Native Razorpay Native Integration

Yagyesh Bobde26 May 20266 min read
Razorpay Sucess Rate Increase By 300% | React Native Razorpay Native Integration

You wire up Razorpay Custom UI. You render your own branded checkout. You query the device for installed UPI apps — GPay, PhonePe, Paytm — so you can show the user nice big intent buttons.

And then a real chunk of your users open checkout and see… nothing. No UPI buttons. Just card and netbanking. They bounce.

You check their device. GPay is right there on the home screen.

That was us — for weeks — until we stopped trusting the community wrapper and rewrote the bridge ourselves.

The Bug: Two Sources of Truth, Wildly Disagreeing

Here's the setup. To render Custom UI with UPI intent buttons, you need to know which UPI apps the user actually has. We were asking two ways and cross-checking:

One — React Native's `Linking.canOpenURL` against `upi://` and the per-app schemes (`tez://`, `phonepe://`, `paytmmp://`).

Two — Razorpay's own "supported UPI intent apps" method exposed through the `react-native-customui` package they maintain.

These should agree. They didn't. Not even close.

`Linking` would happily confirm GPay + PhonePe + Paytm installed — and Razorpay's method would return an empty array. Zero UPI apps. For a user who clearly had three.

Then we pulled the actual number from Mixpanel and it was uglier than we thought. Average UPI apps detected per checkout was 0.06. Six-hundredths. Most users were getting *zero* buttons rendered — even when their phone was full of UPI apps.

Why This Matters — The Silent Bleed In Your Checkout

When Razorpay's method returns empty, your Custom UI has nothing to render. The user gets pushed to card / netbanking. Or worse — a half-rendered screen, a confused tap, a closed tab.

UPI is the single highest-converting payment method in India. Pushing UPI-capable users into card flows isn't a UX paper cut — it's revenue on the floor.

KEY INSIGHT
Nothing is on fire. No crash. No exception. Just a quiet "no UPI apps available, sorry" lie at the JS layer. You'd never know unless you measured it.

Fix 1 — Razorpay Standard SDK. Not A Win. A Tourniquet.

We did the boring quick thing first. Swapped Custom UI for Razorpay's Standard SDK flow.

I want to be honest about this — because the clean narrative ("we switched and numbers shot up") is not what the data says.

Standard SDK handles UPI detection internally, so it stopped the absolute zero-buttons disaster. But our backend-measured payment success rate during that window sat around 3–8%. Not a lift. A hold. It kept the funnel *alive* while we figured out the real fix.

It also wasn't a long-term answer for Kavana. Standard SDK is *their* UI — their typography, their spacing, their flow. For us, checkout is a brand surface. Handing it off to a third party was always a temporary call.

So: Standard SDK got us off the broken Custom UI path. It didn't fix detection. It bought us the time to build the right thing.

Fix 2 — Read The Source. Then Throw It Out.

Before re-wiring the wrapper, I sat down and read it. Properly. End to end.

What I found was — to put it gently — not load-bearing engineering.

  • Silent catches. Promise rejections swallowed in try/catch with no rethrow, no log, no surfaced error. The JS side never knew anything broke.
  • Stale / empty fallbacks. When the native UPI lookup failed (and it did, on certain OEM ROMs under certain Android versions), the package returned an empty list with no signal that the lookup had errored. Empty looked identical to "user has no UPI apps."
  • Bridge serialization losses. Intent metadata the native SDK actually had — package name, app label, icon URI — was getting flattened or dropped across the old RN bridge.
  • Threading. Callbacks resolving off the JS thread, occasionally with null payloads.

Half a dozen tiny silent failures, each individually defensible, all stacking into one big "Razorpay says no UPI apps" lie. This is the part nobody writes about — the community wrapper looks fine until you read it.

Fix 3 — Native Module.

So we rewrote it. Kotlin. Following the official native module docs.

The principle was simple — own every step from the OS up to the JS promise. No silent catches. No stale fallbacks. No third-party serialization quirks.

What actually moved the needle:

Android — query the OS directly. Instead of trusting the wrapper, we used `PackageManager.queryIntentActivities` against a properly-constructed `upi://pay` `Intent`. Then cross-referenced against Razorpay's supported-app list. Two checks, one source of truth — ours.

iOS — declare the schemes, then ask. `canOpenURL` is gated by `LSApplicationQueriesSchemes` in `Info.plist`. The wrapper had a partial list — newer UPI apps weren't even queryable. We declared every scheme we cared about, then queried each explicitly.

Promise resolution on the JS thread, explicitly. Every callback resolves via the standard RN module utilities. No more racey null payloads.

Typed errors, not flattened strings. Razorpay surfaces about a dozen distinct error codes — network, gateway, user cancel, invalid order, expired link. We bubble each one as a typed JS error instead of squashing to a string. Retry flows became sane.

No silent catches. Anywhere. If we don't know what an exception means, it bubbles up to Crashlytics. We'd rather see a report than a confused user.

What Actually Changed — The Real Numbers

This is where it stops being a story and starts being a graph:

  • UPI apps detected per checkout: 0.06 → 1.85 average. Roughly a 30× improvement in the thing that was actually broken. Users who had UPI apps installed now consistently see UPI apps in checkout.
  • Backend-measured payment success rate: ~5% → ~16%. A ~3× lift, peaking at 17% during the best week post-rollout.
  • `upi_apps_present` events: 20–28K / week → ~60K / week. Roughly 3× more checkouts now actually render UPI options. That's not the same users converting better — that's a whole category of checkouts that used to silently fail to render anything.
  • "Checkout opened but no payment method visible" — a real category we used to have — collapsed to near-zero.

The biggest unlock wasn't a single metric — it was *knowing the answer*. When something breaks in checkout now, I read our module's logs and find it. I don't have to spelunk through someone else's wrapper to figure out which silent catch ate my error.

Two Takeaways, Both Bought With Sleep

If two sources of truth disagree on environment state, you don't have a quirk — you have a bug. When `Linking` and Razorpay disagreed on whether GPay was installed, the temptation was to pick the safer number and ship. Wrong call. The disagreement *was* the signal. We should have torn into the wrapper the day we saw it — not weeks later.

Stop trusting community wrappers on anything revenue-shaped. Logging, analytics, image caching — community wrappers are fine. Checkout? The thing that touches money? Own the bridge. The native SDKs from Razorpay (and Stripe, and PayPal) are well-documented enough that you can wire them yourself in a weekend. The wrapper isn't saving you a weekend — it's costing you a quarter of bounces you can't explain.

Don't read the bridge after it breaks. Read it before you ship. Part 2 more focused technically coming soon.

*Keep learning & keep building* ✌️

Written by

Yagyesh Bobde

React NativeRazorpayAnalyticsAndroid App DevelopmentSoftware Engineering

Originally published on

Read this article on Medium