記事一覧に戻る
CapacitorのiOSでリモートサーバーを使うとOAuth認証が動かない

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がサーブするローカルアセットである前提で設計されています。具体的には:

  1. ネイティブ側でイベントが発生
  2. Capacitorがローカルサーバー経由でJavaScriptにメッセージを送信
  3. 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通信が必要な機能があるか
  • 必要に応じてネイティブコードでの回避策を実装できるか