Fix UPI Deep Link Page Reloads and Timer Resets in JSP
In a developer community I participate in, a user recently ran into a frustrating issue: while integrating a UPI payment gateway on a JSP-based mobile site, triggering the upi:// deep link caused the browser to either refresh the page or navigate back to the previous screen. This immediately killed their JavaScript-based countdown timer, breaking the payment flow.
The Original Question
Section titled “The Original Question”“I have a JSP checkout page with a 5-minute countdown timer. When the user clicks ‘Pay via UPI’, we trigger a deep link (e.g., upi://pay...). On many mobile devices, as soon as the UPI app selection popup appears or the user switches to the payment app, the original browser tab reloads or navigates back, causing the timer to reset to 5:00. How do I keep the timer running or prevent the reload?”
Immediate Fix: Persistence over Prevention
Section titled “Immediate Fix: Persistence over Prevention”The most reliable way to handle this is to stop relying on volatile JavaScript memory for your timer. Because mobile OSs often “freeze” or “discard” background tabs to save RAM, you must persist the timer’s end-state.
// Version: ES6+ (Illustrative example — verify in your environment)
const TIMER_DURATION_SECONDS = 300; // 5 minutes
function startTimer() { // 1. Check if an end-time already exists in localStorage let expiryTime = localStorage.getItem('payment_expiry');
if (!expiryTime) { // 2. If not, set it based on current time + duration expiryTime = Date.now() + (TIMER_DURATION_SECONDS * 1000); localStorage.setItem('payment_expiry', expiryTime); }
const timerInterval = setInterval(() => { const currentTime = Date.now(); const remaining = Math.max(0, Math.floor((expiryTime - currentTime) / 1000));
if (remaining <= 0) { clearInterval(timerInterval); handleTimeout(); }
updateTimerDisplay(remaining); }, 1000);}
function updateTimerDisplay(seconds) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; document.getElementById('timer').innerText = `${mins}:${secs < 10 ? '0' : ''}${secs}`;}
function handleTimeout() { localStorage.removeItem('payment_expiry'); alert("Payment window expired."); window.location.href = "/timeout-error.jsp";}
// Call on page loadstartTimer();Detailed Explanation: Why the Reload Happens
Section titled “Detailed Explanation: Why the Reload Happens”When a user triggers a upi:// intent, the browser loses focus. Mobile browsers (especially Chrome on Android and Safari on iOS) handle this “loss of focus” in several ways that can trigger a reload:
- Memory Pressure: The UPI app (Google Pay, PhonePe, etc.) is heavy. The OS may kill the browser process to free up RAM. When you return to the browser, it performs a “hard reload” of the last URL.
- Navigation Logic: If the deep link is triggered via a standard
<a href="...">or a form submission, some browsers interpret the “App Switcher” as a navigation event. - Page Visibility API: When the tab goes into the background, timers (
setInterval) are often throttled or paused by the browser’s battery-saving engine.
Solution 1: Use an Iframe Trigger (The “Silent” Intent)
Section titled “Solution 1: Use an Iframe Trigger (The “Silent” Intent)”Instead of changing window.location or using an anchor tag—which signals the browser that you are navigating away—trigger the deep link inside a hidden iframe. This keeps the parent JSP context “stable.”
Version: Vanilla JavaScript (Compatible with most JSP/HTML environments)
function triggerUPIDeepLink(url) { // Create a hidden iframe const iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = url;
// Append to body to trigger the intent document.body.appendChild(iframe);
// Clean up after a delay setTimeout(() => { document.body.removeChild(iframe); }, 2000);}
// Usage// triggerUPIDeepLink("upi://pay?pa=...&am=...");Note: Some modern mobile browsers are tightening security on iframe-initiated deep links. Test this across your target devices.
Solution 2: Page Visibility API Management
Section titled “Solution 2: Page Visibility API Management”If you must keep the timer running in the background (or resume it accurately), use the visibilitychange event to detect when the user returns from the UPI app.
Version: ES6 / Standard Web API
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { console.log("User returned to the browser. Re-syncing timer..."); // Re-run your startTimer logic here to sync with localStorage syncTimer(); } else { console.log("User switched to UPI app."); }});Edge Cases and Common Pitfalls
Section titled “Edge Cases and Common Pitfalls”1. The “Back” Button Issue
Section titled “1. The “Back” Button Issue”If the user completes the payment and the UPI app sends them back, the browser might try to load a cached version of the JSP. To prevent issues with the timer showing old data, ensure your JSP headers prevent caching:
<% response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1 response.setHeader("Pragma", "no-cache"); // HTTP 1.0 response.setDateHeader("Expires", 0); // Proxies%>2. LocalStorage Cleanup
Section titled “2. LocalStorage Cleanup”If the user leaves the checkout page voluntarily (e.g., clicks the ‘Home’ button), the payment_expiry will stay in localStorage. Always clear the storage key when the payment is successfully completed or when the user arrives at the “Thank You” page.
Related Follow-up Questions
Section titled “Related Follow-up Questions”Q: Does this happen because of the JSP server-side state?
No. The reload is almost entirely a client-side (browser) behavior. However, if your JSP relies on HttpSession and the browser session cookie is marked as SameSite=Strict, switching apps can sometimes cause session issues on the return trip. Ensure your session cookies are set to SameSite=Lax.
Q: Can I use sessionStorage instead of localStorage?
I recommend localStorage. sessionStorage is tied to the specific tab session. If the mobile browser decides to kill the tab and recreate it upon return (a common occurrence on low-RAM Android devices), sessionStorage is often wiped, whereas localStorage persists.
Q: Why doesn’t setInterval work in the background?
Mobile browsers pause the main JS thread when a tab is not visible to preserve battery. You can never trust setInterval to count accurately while the user is inside a UPI app. Always calculate the difference between the current system time and a stored end-time.