CapacitorのiOSでリモートサーバーを使うとOAuth認証が動かない
Capacitorでserver.urlを設定してリモートサーバーに接続すると、CapacitorのApp.addListenerが発火しなくなる問題に遭遇。原因と回避策を解説。
CapacitorでiOSアプリを開発していて、ちょっとハマった問題があったので共有します。
やりたかったこと
PWAをCapacitorでネイティブアプリ化する際、開発効率を上げるために以下の構成にしたいと考えました。
- 開発時: リモートサーバー(
https://stg.example.com)をWebViewで表示 - 本番: 同じくリモートサーバー(
https://example.com)を表示
Capacitorの capacitor.config.ts には server.url というオプションがあり、これを設定するとローカルのアセットではなくリモートURLを読み込めます(Live Reload機能)。
const config: CapacitorConfig = {
appId: "com.example.app",
appName: "MyApp",
webDir: "dist",
server: {
url: "https://stg.example.com",
},
};
これで毎回ビルドしなくても、サーバー側の変更がすぐアプリに反映されて便利…のはずでした。
問題:OAuth認証後のコールバックが発火しない
Google/Apple Sign Inを実装していて、OAuth認証後にアプリに戻ってくるフローがあります。
通常、Capacitorでは App.addListener("appUrlOpen") でディープリンクを受け取ります。
import { App } from "@capacitor/app";
App.addListener("appUrlOpen", (data) => {
console.log("URL opened:", data.url);
// OAuth認証後の処理
});
ところが、server.url を設定した状態だと、このイベントが一切発火しません。
Xcodeのログを見ても、AppDelegateの application(_:open:options:) は呼ばれているのに、JavaScript側のリスナーには何も届かないのです。
原因:Capacitorブリッジの仕組み
調べてみると、これはCapacitorの仕様によるものでした。
Capacitorのネイティブ→JavaScript通信は、WebViewで読み込まれているページがCapacitorがサーブするローカルアセットである前提で設計されています。具体的には:
- ネイティブ側でイベントが発生
- Capacitorがローカルサーバー経由でJavaScriptにメッセージを送信
- JavaScript側のリスナーが呼ばれる
しかし、server.url でリモートサーバーを指定すると、WebViewはリモートのHTMLを表示しています。Capacitorのローカルサーバーとは別オリジンになるため、ネイティブ→JS通信が機能しなくなります。
回避策:NotificationCenterとWebViewリダイレクト
Capacitor経由でイベントを受け取れないので、ネイティブコードで直接対応しました。
1. AppDelegateでOAuthコールバックを検知
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// OAuth認証コールバックの場合、独自の通知を発行
if url.path.contains("auth/callback") {
NotificationCenter.default.post(
name: Notification.Name("OAuthCallback"),
object: nil,
userInfo: ["url": url]
)
}
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
2. ViewControllerで通知を受け取りWebViewをリダイレクト
override func capacitorDidLoad() {
super.capacitorDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleOAuthCallback(_:)),
name: Notification.Name("OAuthCallback"),
object: nil
)
}
@objc private func handleOAuthCallback(_ notification: Notification) {
guard let url = notification.userInfo?["url"] as? URL else { return }
// サーバーURLの/auth/callbackにトークンを付けてリダイレクト
var components = URLComponents()
components.scheme = "https"
components.host = "stg.example.com"
components.path = "/auth/callback"
components.query = url.query
components.fragment = url.fragment
guard let callbackUrl = components.url else { return }
webView?.load(URLRequest(url: callbackUrl))
}
3. Web側でURLからトークンを取得
// /auth/callback ページ
const hashParams = new URLSearchParams(window.location.hash.substring(1));
const accessToken = hashParams.get("access_token");
const refreshToken = hashParams.get("refresh_token");
if (accessToken && refreshToken) {
await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken,
});
}
この方法のメリット・デメリット
メリット
- Capacitorのプラグイン機構に依存しない
- シンプルなURLリダイレクトなので動作が安定
- Web版と同じ認証フローを使える
デメリット
- ネイティブコード(Swift)の実装が必要
- Capacitor以外のプラグインも同様に動かない可能性がある
まとめ
Capacitorの server.url は便利な機能ですが、ネイティブプラグインとの連携が必要な場合は注意が必要です。
特にOAuth認証のようなディープリンクを使う機能は、App.addListener("appUrlOpen") が発火しないため、独自の回避策が必要になります。
リモートサーバーを使う場合は、以下を事前に確認しておくとよいでしょう。
- 使用するCapacitorプラグインがリモートURL環境で動作するか
- ディープリンクやプッシュ通知など、ネイティブ→JS通信が必要な機能があるか
- 必要に応じてネイティブコードでの回避策を実装できるか