プロジェクトの納期まであと2週間という段階で、クライアントが選定した国内PG(Payment Gateway)には、Flutter用のSDKが存在しなかった。あるのはWeb用のJavaScript SDKと、古びたドキュメントだけだ。多くの開発者がここで絶望するが、我々のチームはWebView(ウェブビュー)を使ってこれを解決することにした。しかし、単純にURLをロードするだけでは、PayPayやLINE Payといった外部アプリへの遷移(Deep Link)や、3Dセキュアの認証フローで必ず躓くことになる。本記事では、実際に本番環境で稼働している「SDKなしでの決済連携」の完全な実装パターンを共有する。
Deep Dive Analysis: なぜ標準WebViewで決済が失敗するのか
Flutter公式の webview_flutter パッケージを使用すれば、Webベースの決済画面を表示すること自体は容易だ。しかし、決済プロセスにおいて致命的な問題が発生するのは、ユーザーが「支払う」ボタンを押した瞬間である。
多くのモダンなPGは、決済アプリ(PayPay, 楽天ペイなど)を起動するために、`http://` や `https://` ではないカスタムURLスキーム(例: `paypay://`, `intent://`)を使用する。標準のWebView実装はこれらのプロトコルを理解できず、`ERR_UNKNOWN_URL_SCHEME` エラーを吐いてクラッシュするか、単にホワイトアウトする。
この問題を解決するには、WebViewのナビゲーションリクエストをインターセプト(傍受)し、HTTP以外のリクエストが来た場合にOSのネイティブ機能(IntentやURL Scheme)へハンドリングを委譲する必要がある。これが、SDKなしで堅牢な決済システムを構築するための唯一の解だ。
The Solution: NavigationDelegateによる完全制御
解決策は、`NavigationDelegate` の `onNavigationRequest` コールバック内にロジックを集中させることだ。ここでは、url_launcher パッケージを併用して外部アプリへのブリッジを作成する。
以下は、実際に私が本番環境で使用した修正版のコードスニペットである。
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();
// WebViewControllerの初期化と設定
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
// ページの読み込み開始時
onPageStarted: (String url) {
debugPrint('Page started loading: $url');
},
// 【重要】ナビゲーションリクエストの制御
onNavigationRequest: (NavigationRequest request) async {
final Uri uri = Uri.parse(request.url);
// 1. http/httpsの場合は通常通りWebView内で遷移させる
if (uri.scheme == 'http' || uri.scheme == 'https') {
return NavigationDecision.navigate;
}
// 2. それ以外のカスタムスキーム(paypay://, line:// など)の場合
// 外部アプリとしての起動を試みる
if (await canLaunchUrl(uri)) {
await launchUrl(
uri,
mode: LaunchMode.externalApplication, // 外部アプリで開く
);
// WebView内の遷移はブロックする(エラー回避)
return NavigationDecision.prevent;
}
// 3. AndroidのIntentスキームなどの特殊ケース処理(必要に応じて実装)
// ここでフォールバック処理を入れることも可能
debugPrint('Could not launch ${request.url}');
return NavigationDecision.prevent;
},
),
)
..loadRequest(Uri.parse(widget.paymentUrl));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('決済画面')),
body: WebViewWidget(controller: _controller),
);
}
}
| シナリオ | デフォルト挙動 | 修正後の挙動 |
|---|---|---|
| 通常のクレカ入力画面 | 表示される | 表示される |
| PayPayアプリ起動 | エラー (net::ERR_UNKNOWN_URL_SCHEME) | 成功 (アプリが起動) |
| 3Dセキュア認証 | 一部銀行でブロックされる可能性あり | 正常に遷移 |
この実装により、PG側が「アプリ決済」を選択した場合でも、WebViewが適切にハンドリングを放棄し、OS側に処理を渡すフローが確立された。これは、特定のPGに依存しない汎用的なアプローチである。
Conclusion
Flutter専用のSDKが提供されていないからといって、そのPGの採用を諦める必要はない。WebViewと適切に構築された `NavigationDelegate` があれば、ほぼ全ての決済フローをネイティブアプリ内で完結させることが可能だ。
重要なのは、WebViewを単なる「ブラウザ」として扱うのではなく、アプリと外部サービスを繋ぐ「制御可能なゲートウェイ」として設計することだ。次回、クライアントが無茶なPG指定をしてきたときは、このコードスニペットを思い出してほしい。
Post a Comment