← Back to list

Why Stripe Crashed My Flutter App on Some Phones and How I Fixed It

I started with a common setup. I opened the payment page inside my Flutter app with WebView. That approach worked well for MyFatoorah…

Anab khan · 2026-04-21 19:34 · 0 claps · 6.6 min read
#flutter #stripe-integration #payment-gateway #deep-linking #flutter-webview
Open on Medium ↗
Wiki topics: FIN · Fintech & Banking 📱 · Mobile Development

Why Stripe Crashed My Flutter App on Some Phones and How I Fixed It

I started with a common setup. I opened the payment page inside my Flutter app with WebView. That approach worked well for MyFatoorah. Stripe looked fine at first. The page opened, users entered card details, and payment went through. Then the return flow started failing.

On some Android phones, Stripe returned to the app and the flow finished. On other phones, the app restarted after payment. On one phone, the whole OS crashed. On some runs, the browser got stuck on the return page. On others, the backend completed the order, cleared the cart, and the app still never showed the success screen.

This article explains what broke, why WebView behaved differently for Stripe and MyFatoorah, and how I replaced that flow with a browser-based return path, a backend callback, and deep linking.

THE FIRST VERSION

My first version used Flutter WebView for the payment page. The app opened the Stripe Checkout URL inside the app. I watched the return URL. When I saw a success path, I popped the payment screen and moved forward.

This setup looked clean. The user stayed inside the app. The UI felt direct. For MyFatoorah, that path stayed stable in my app. For Stripe, the result changed from phone to phone.

That difference mattered. MyFatoorah worked as a lighter, more stable web flow in my case. Stripe hosted a heavier page with more scripts, more redirect work, and more activity during the final return step. My logs showed hCaptcha activity, long frame stalls, GPU pressure, and surface churn near the end of the payment flow. Flutter’s Android WebView implementation uses Texture Layer Hybrid Composition, and the package exposes a switch to full Hybrid Composition for edge cases. That helped on some runs, but it did not solve the root issue on the failing phone.

WHAT BROKE

The Stripe page often loaded. The user paid. The backend processed the order. The cart cleared on the server. Then the app failed during the return step.

Three patterns appeared.

The first was a post-payment crash. Payment finished. The callback ran. Then the app died during redirect or WebView teardown.

The second was a browser stall. The backend finished the order, but the browser stayed on the callback page or on a browser warning screen. The app never resumed with the right state.

The third was a system crash on one Android phone. My logs ended with DeadSystemException. Android defines that as the core Android system dying and restarting. When that happens, every running app gets killed. That matched what I saw on the device.

WHY WEBVIEW FAILED ON SOME PHONES

The short answer is page weight combined with device differences.

Stripe Checkout is a hosted payment page. In my flow, the hard part was not the first load. The hard part was the last mile. That part included page scripts, risk checks, hCaptcha frames, redirect handling, WebView teardown, and a route change inside Flutter. One phone handled the load. Another phone did not.

That explains why the same code looked fine on one Android device and failed on another. The package, the page, and the redirect path were the same. The device graphics stack and WebView implementation were not.

I tried the usual fixes. I removed extra layers around the WebView, forced Hybrid Composition, blanked the page before close, and delayed the pop. Those changes improved the flow, but only up to a point. On the bad phone, the system still crashed. At that point, the right move was to stop asking WebView to carry the return path.

WHY MYFATOORAH WORKED WHILE STRIPE DID NOT

This was a practical observation from my app, not a rule for every app.

MyFatoorah stayed stable inside WebView for me. Stripe did not. The most likely reason was the shape of the page and the return sequence. Stripe’s hosted page did more work near the end. My logs showed more rendering pressure and more activity around the final transition. That was enough to expose weak points in one device’s Android WebView path.

Do not assume one payment provider proves WebView is safe for every other provider.

THE TURNING POINT

The real clue came from the backend.

I saw completed orders on the server. I saw cart clearing on the server. That told me payment success itself was not the main problem anymore. The weak point was the handoff from browser back to app.

Once I saw that, the design changed. I stopped trying to keep Stripe inside the app. I moved the payment page to the device browser. Then I focused on the return path.

THE NEW FLOW

The final flow looked like this.

The app placed the order and received a payment URL. Flutter opened that URL in the browser with url_launcher. The user paid in the browser. Stripe returned through a backend callback. That callback completed the payment flow and redirected back into the app through a custom URL such as zapesim://payment/success?order_id=… The app listened for that deep link, resumed, and opened the result flow. Then the app fetched the order from the API and showed the final success or failed screen.

That design moved the heavy web page out of Flutter. It also split payment success from app return success. That split was important.

WHAT FLUTTER DID IN THE NEW FLOW

On the Flutter side, the key change was url_launcher. The package passes the URL to the host platform. For web URLs, the host platform opens the default browser. The package also supports browser versus in-app handling through LaunchMode. I used the browser path for payment so the heavy page no longer lived inside Flutter.

I wrapped that in a small launcher service. The service started listening for the app deep link before opening the browser. Then it opened the payment URL. When the app received zapesim://payment/success?… or zapesim://payment/failed?…, the service completed the payment outcome and returned control to the checkout flow.

That solved the device crash. The app no longer rendered Stripe inside WebView. The browser owned the page. Flutter only owned the return.

WHAT THE BACKEND DID

On the backend side, the callback completed the payment flow and redirected back into the app with a custom URL.

Success worked like this. Stripe called: https://my-domain/callback?session_id=...

The backend then sent the app to: zapesim://payment/success?order_id=…

Failure used the same idea with a failed path.

The key point was simple. Stripe did not need to know about the app URL. Stripe only needed the server callback. The server callback handled the handoff from web back to app.

WHY THE APP STILL FETCHED THE ORDER

The deep link told the app where to go next. It did not serve as the final source of truth.

The app still fetched the order after return. That gave me the final order payload, item data, currency, and status details. It also solved a UI problem. I no longer needed to guess from the deep link. The result screen rendered from the same API shape as the rest of the app.

DEEP LINKING: THE PART THAT CAUSED A SECOND BUG

After the browser-based flow started working, a second problem appeared.

The app received the deep link, but GoRouter also tried to route the same URI. That blew away the checkout screen, closed the cubit, and caused emit-after-close errors.

Flutter documents this behavior. If you use a plugin to handle deep links yourself, you should turn off Flutter’s default deep link handler. Flutter also notes that Router may replace the current set of pages when a new deep link opens while the app is running.

So I disabled Flutter’s default deep link handling and let app_links own the payment return path. That stopped the route replacement issue.

WHAT THE USER SAW AFTER THE FIX

When the app returned from the browser, the flow paused for a moment before the final state appeared.

The issue came from the order check happening too early in the return flow. I moved that status check to the final step of the payment journey.

The flow then became: the browser closed, the app returned at once, the app checked the latest order state, and the app showed the final payment state from that response. This removed the extra stop in the checkout flow.

WHY THIS APPROACH WORKED BETTER

The browser-based flow solved the hard part by removing Stripe Checkout from Flutter. That cut out the unstable WebView teardown path. The deep link gave the app a clear entry point after payment. The order API gave the app a single source for the result screen.

Each step had one job. The browser handled the payment page. The deep link handled the return. The order API handled the final state. That separation made the whole flow easier to reason about and easier to debug.

THE PRACTICAL LESSONS

WebView is not a universal answer for payments. One provider may stay stable. Another may break on specific phones.

Payment success and app return success are two different steps. Your backend may finish the order while your app still fails to return cleanly.

The browser is a safer place for a hosted payment page. Your app owns the launch and the return. The browser owns the page.

The deep link is a return signal. Your order API and backend state remain the final truth.

If you use plugin-based deep linking in Flutter, turn off Flutter’s default deep link handler. If you skip that step, your router may replace pages at the wrong time.

THE FINAL ARCHITECTURE

Flutter placed the order. Flutter opened the payment URL in the browser. Stripe returned through the backend callback. The app resumed through app_links. The payment result screen fetched the order. The screen rendered the final result.

That design gave me a stable flow across devices. It removed the WebView crash path. It kept the UI clean.

If the server shows a completed order while the app still misses success, payment is not your main problem anymore. The return flow is. That shift is what fixed the flow for me.


메타데이터
post_id
8355317ada4b
slug
why-stripe-crashed-my-flutter-app-on-some-phones-and-how-i-fixed-it-8355317ada4b
url
https://medium.com/@anabkhan8148/why-stripe-crashed-my-flutter-app-on-some-phones-and-how-i-fixed-it-8355317ada4b
canonical_url
https://medium.com/@anabkhan8148/why-stripe-crashed-my-flutter-app-on-some-phones-and-how-i-fixed-it-8355317ada4b
author_url
https://medium.com/@anabkhan8148
status
ok
fetched_at
2026-06-28 10:39:35