When adding payment functionality to a mobile app, the standard operating procedure is to pull the native SDK provided by the Payment Gateway (PG). It’s clean, it’s documented, and it works. But reality often deviates from the ideal path. Recently, we faced a project requirement involving a specific domestic PG that simply did not offer a Flutter SDK. We were left with their raw web integration guide and a deadline.
This article documents how we bypassed the lack of an SDK by architecting a robust payment flow using WebView. We will cover the specific technical hurdles, primarily handling deep links and intent schemes, to ensure a seamless transaction experience.
The "Intent" Trap: Why Standard WebViews Fail
The core challenge isn't rendering the payment page; it's what happens during the transaction. Modern PGs (especially in Asia) rely heavily on App-to-App authentication. They attempt to launch banking apps or digital wallets (like Venmo, KakaoPay, or Line Pay) to verify the user.
In a standard desktop browser, this is handled via redirects. In a mobile webview_flutter instance, however, the WebView controller often chokes on unknown URL schemes. If the PG redirects to intent://... or a custom scheme like kakaotalk://, the WebView doesn't know how to handle it, resulting in a ERR_UNKNOWN_URL_SCHEME or simply a blank screen.
intent:// schemes. You must manually parse the intent string to extract the package ID and the browser_fallback_url. Failure to do this means the user is stuck if they don't have the banking app installed.
Furthermore, managing the session state (cookies) between the Flutter app and the PG's redirection logic is vital to ensure the final "Success" or "Failure" callback is captured correctly.
The Solution: Custom Navigation Delegate
To fix this, we need to intercept every navigation request within the WebView. We use the NavigationDelegate to inspect the URL before the WebView attempts to load it. If the URL is a standard http/s link, we allow it. If it is an external app scheme, we delegate the action to the OS using url_launcher.
The following code demonstrates a production-ready implementation that handles Android Intents and general app schemes.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
class PaymentWebView extends StatefulWidget {
final String paymentUrl;
const PaymentWebView({Key? key, required this.paymentUrl}) : super(key: key);
@override
State<PaymentWebView> createState() => _PaymentWebViewState();
}
class _PaymentWebViewState extends State<PaymentWebView> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) async {
final url = request.url;
// 1. Handle HTTP/S requests normally (allow navigation)
if (url.startsWith('http://') || url.startsWith('https://')) {
// Check for specific Success/Fail callback URLs from your PG
if (url.contains('payment/success')) {
// Handle success logic
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
}
// 2. Handle Android Intent Schemes (The tricky part)
if (Platform.isAndroid && url.startsWith('intent://')) {
try {
// Attempt to launch the app via intent
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
return NavigationDecision.prevent;
} catch (e) {
// Fallback: Parsing the intent to find browser_fallback_url
// In a real scenario, you'd parse the string manually or use a regex
// to extract 'browser_fallback_url' parameter.
final fallbackUrl = _extractFallbackUrl(url);
if (fallbackUrl != null) {
await launchUrl(Uri.parse(fallbackUrl));
}
return NavigationDecision.prevent;
}
}
// 3. Handle other Custom Schemes (e.g., kakaotalk://, ispmobile://)
// Just try to launch them externally
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
return NavigationDecision.prevent;
}
return NavigationDecision.prevent;
},
),
)
..loadRequest(Uri.parse(widget.paymentUrl));
}
String? _extractFallbackUrl(String intentUrl) {
// Simple parser logic for demo purposes
// Real implementation requires robust parsing of S.browser_fallback_url
if (intentUrl.contains('browser_fallback_url=')) {
return Uri.decodeFull(intentUrl.split('browser_fallback_url=')[1].split(';')[0]);
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: WebViewWidget(controller: _controller),
);
}
}
LSApplicationQueriesSchemes in your Info.plist. You must allow list the specific schemes (like `kftc-bankpay`, `ispmobile`) that the PG uses, otherwise canLaunchUrl will return false even if the app is installed.
| Scheme Type | Example | Action Required |
|---|---|---|
| Standard Web | https://pg.com/pay |
NavigationDecision.navigate (Allow load in WebView) |
| Deep Link | kakaotalk://... |
launchUrl (Open external app) + NavigationDecision.prevent |
| Android Intent | intent://... |
Parse Intent structure → launchUrl or fallback |
Conclusion
Implementing a PG integration in Flutter without an official SDK forces you to understand the underlying mechanics of mobile web navigation. While it feels like a hack, wrapping the payment flow in a WebView and manually handling the OS-level intents is a battle-tested strategy that works for almost any payment provider.
The key takeaway is to aggressively filter the URLs in the NavigationDelegate. Do not trust the WebView to handle custom schemes automatically. By explicitly handing off authentication requests to the OS via url_launcher, you bridge the gap between the web payment interface and the native app ecosystem.
Post a Comment