growth
UPI Checkout Loading-State Design That Prevents Duplicate Orders
Design create-order, app-launch, verification, and recovery loading states that explain progress, block unsafe repeated actions, and preserve one payment attempt.
“Loading” is not one payment state. A checkout can be calculating price, creating a local order, requesting a gateway attempt, opening a payer app, waiting for authenticated payment evidence, or recovering an unknown network result. One generic spinner hides these distinctions and encourages customers to tap again.
Design progress around what the system knows and what action remains safe.
Model Loading as Real States
Use explicit frontend states backed by server state where relevant:
| UI state | Meaning | Allowed action |
|---|---|---|
| Preparing order | Trusted backend is pricing and creating | Cancel navigation if safe; no second create |
| Payment ready | QR or Intent is available | Start payment |
| Opening UPI app | External navigation requested | Offer fallback after a bounded wait |
| Checking payment | Customer may have acted; backend is verifying | Refresh known order, do not create another |
| Result unknown | Network outcome is uncertain | Reconcile original reference |
| Verified | Authenticated evidence matched | Continue to fulfilment or receipt |
| Review required | Evidence is missing or mismatched | Show support reference |
Render the state as readable text, not only animation. A spinner needs a label and must not be announced continuously to assistive technology.
Protect Order Creation
Disable the create action synchronously on the first activation. Generate a stable backend client_txn_id, save the local order under a uniqueness constraint, and reuse that identity if the network request is retried.
Do not rely on disabled UI alone. Two tabs, two devices, a slow double tap, or a browser retry can still reach the backend concurrently. The server must make duplicate submissions return the existing attempt or a controlled conflict.
Keep the amount server-owned. If the cart changes while the request is in progress, finish or cancel the original attempt through the state machine before creating another priced order.
Separate Launch From Verification
After the payment surface is ready, “Pay with UPI” may open an external app. That action is navigation. Page blur, visibility change, a redirect, or the customer returning does not prove success.
Use copy such as:
- “Opening your UPI app…” during the launch request.
- “Could not open an app—try another way” when navigation fails.
- “Checking payment—do not pay again” after return.
- “Payment verified” only after backend confirmation.
The mobile Intent fallback guide covers the recovery ladder.
Recover From Slow and Unknown Results
Set a UI timeout for communication, not for declaring the financial result. When create-order or verification takes longer than expected, preserve the order reference and ask the backend for its current state.
Provide an exact next action: continue waiting, retry the same request, switch payment surface on the same attempt, open a support case, or create a replacement after the first attempt is formally closed. Avoid “Something went wrong” when the result is actually unknown.
If the customer closes the page, a signed event or scheduled reconciliation should still resolve the backend order. The browser cannot be the only component responsible for completion.
Test Repeated Actions
Test rapid double click, Enter plus click, two tabs, back/forward navigation, mobile app return, slow API response, response lost after backend commit, and page reload during each state.
Assert one local order, one active client_txn_id, one final payment transition, and one fulfilment effect. Verify focus moves to a useful status only when necessary and that disabled controls explain why they are unavailable.
Measure repeated-action attempts, create-order duplicates prevented, time in each state, unresolved results, and verified conversion. Faster animation is not the goal; a clear, duplicate-safe payment journey is.
Use a copy matrix so the same state has the same meaning across mobile, desktop, support, and analytics:
| Backend state | Primary copy | Unsafe copy to avoid |
|---|---|---|
| Creating | “Preparing your payment” | “Payment processing” |
| Pending | “Checking payment” | “Almost successful” |
| Unknown | “Confirming this order” | “Try paying again” |
| Review | “Payment needs review” | “Payment lost” |
| Paid | “Payment verified” | “App returned successfully” |
Review translations against these meanings. A shorter translated label must not turn uncertainty into failure or verification into a browser-only success signal.
Direct answers
Frequently asked questions
- Should the pay button remain disabled after a request timeout?
- Do not leave it disabled forever or immediately create another order. Show an unknown-result state while the backend reconciles the original reference, then provide the correct next action.
- Does returning from a UPI app end the loading state?
- It ends app navigation, not payment verification. Show a distinct checking-payment state until authenticated backend evidence resolves the order.
- How can repeated taps be made safe?
- Disable the action immediately, create the order with a stable client transaction ID, enforce database uniqueness, and make retries reuse that same identity.
Build your payment flow
Explore the API and browser-only merchant tools.
Create UPI checkout orders, verify signed events, or test the free calculators and generators without exposing credentials.