0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

iOS 26 で WKWebView のログインが勝手に消える:起動直後の空の Cookie ストアを「ログアウト」と読み違えていた

0
Posted at

minulo は、iOS 26 以前で WKWebView の Cookie ストアがアプリの起動直後に数秒だけ空で返ることを「ログアウトした」ではなく「まだ分からない」と扱うように変えて、Instagram のログインが勝手に消える不具合を直しました(バージョン 1.0.15)。minulo は Instagram のリールや YouTube のショートを消して DM は残す iPhone アプリで、私はその開発者です。minulo がパスワードや Cookie をどう扱っているかは、こちらにまとめています:https://minulo.app/is-minulo-safe.html?ref=qiita-1101

何をしているアプリか(ログインまわりだけ)

minulo は Instagram などのウェブ版を WKWebView で開きます。ログインは各サービスのページで本人がするので、パスワードはアプリに来ません。

複数アカウントに対応するため、ログイン中の Cookie をアカウントごとに端末の中へ控えておき、開いたときにログインの印(Instagram なら sessionid)が無ければ、控えから戻します。サービス側でログアウトしたときに戻し続けないよう、戻したのにログインできていなければ控えを捨てます。

起きていたこと

1.0.13 のころ、「アプリを閉じるたびに Instagram からログアウトされる」というレビューが日本と韓国から続けて付きました。PostHog の数字では、iOS 26 以前の人は開いた 1,057 回のうち 242 回(22.9%)でログアウトが起き、7日間で開いた人の 29.7% が1回以上ログアウトしていました。iOS 27 では 0 回です。

手元の実機は iOS 27.2 だったので、一度も再現しませんでした。

原因

iOS 26 以前では、アプリを起動し直してから2〜3秒のあいだ、WKWebsiteDataStore.default().httpCookieStore.getAllCookies が、ログイン中でも空の配列を返すことがありました。

minulo は WebView を開く前に「ログインの印があるか」を確かめていて、空の配列を「印が無い=ログアウトした」と読んでいました。そこで控えから Cookie を戻すのですが、控えはそのときのログインより古いことがあります。生きているログインを古い控えで上書きした結果、Instagram 側では無効なログインになり、ログイン画面が出ていました。

つまり、守るための仕組みがログアウトを起こしていました。

直し方

1. 「無い」と「分からない」を分ける

サービスはログアウトしていても、端末を見分ける Cookie などを必ず残します。なので、そのサービスのドメインの Cookie が1つも無いなら、ログアウトではなく「ストアがまだ読み込めていない」と見なして nil を返すようにしました。

AsyncFunction("hasSession") { (domains: [String], names: [String], promise: Promise) in
  WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in
    // そのサービスの Cookie が1つも無い=まだ分からない
    if !cookies.contains(where: { Self.matches($0, domains) }) { promise.resolve(nil); return }
    promise.resolve(cookies.contains { Self.isSession($0, domains, names) })
  }
}.runOnQueue(.main)

2. 開く前は、分からなければ戻さない

開く前の確認では、「無い」が2回続いたときだけ信じ、nil(分からない)のときは戻さずにそのまま開きます。戻すのは今のログインを消す操作なので、迷ったら何もしない側に倒しました。

const r1 = await N.hasSession(c.domains, c.names);
const has = r1 === false ? await N.hasSession(c.domains, c.names) : r1;
if (has === null) { st.unknown = true; }          // 戻さずに開く
if (has === false && N.hasSavedSession(s)) { /* 控えから戻す */ }

3. ページを読んだあとに確かめ直す

開く前に分からなかったときは、ページを読み終えたあとにもう一度確かめます。このときはストアにそのサービスの Cookie が入っているので、印が無ければ本当に消えています。1.5秒あけて2回とも無いときだけ、控えから戻して入口から読み直します(1回の表示で1回まで)。

直ったかをどう測っているか

次のイベントを PostHog に送っています(session_unknown は 1.0.15 で足しました)。どれも数字だけで、URL やアカウントの情報は載せていません。

  • session_unknown:開く前に分からなかった回数と、そのときのストアの Cookie の数
  • session_restored:戻した回数と理由(開く前に戻した/ページを読んだあとに戻した)
  • session_restore_failed:戻したのにログインできていなかった回数と、控えてから何時間たっていたか

判定は「iOS 26 以前の 1.0.15 で、開いた回数あたりのログアウトが1%未満」としています。公開してまだ数日なので、開いた回数が300回を超えるのを待って判断します。

学んだこと

  • 空の結果は「無い」とは限らない。読み込みの途中かもしれない。
  • 自分の実機の OS だけで試すと、古い OS でしか起きない不具合は見えない。使っている人の OS の割合を先に見る。
  • 「戻す」「消す」のような取り返しのつかない操作は、確信が持てるときだけにする。
  • 直す前に、何回起きているかを数えるイベントを入れておくと、直したあとに効き目を同じ物差しで比べられる。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?