Skip to content

sign in with pixiv (oauth + api layer scaffolding)

jedmund requested to merge claude/research-pixiv-login-6ZpCb into main

Created by: jedmund

A first-pass auth + data layer for pixillate. You can sign in with a real Pixiv account, see your user info, and sign out. Sessions persist to the Keychain and are restored on next launch.

What's in here

Seven small commits, roughly bottom-up:

  1. foundationPixivError, DTOs (PixivUser, PixivTokenResponse, PixivTokens), UserAgent, URLSessionProtocol test seam.
  2. pkce + oauth token exchange — PKCE (S256) and the HTTP side of /auth/token for both authorization_code and refresh_token grants.
  3. keychain persistence + token store actorKeychainStore (generic-password blob + TokenPersisting protocol) and the TokenStore actor. TokenStore runs single-flight refresh so concurrent API callers share one round-trip.
  4. authed api clientPixivAPIClient.send<T> with transparent refresh-on-401 retry, fed by a declarative PixivEndpoint enum.
  5. login webviewLoginWebView (UIViewRepresentable + WKNavigationDelegate) catches Pixiv's pixiv://account/login?code=... redirect and extracts the OAuth code. ASWebAuthenticationSession isn't viable here — its callback matcher only fires for URL schemes the app owns.
  6. auth session facade + sign-in/out UI@Observable AuthSession is the whole view-facing contract. ContentView switches on auth.state between a sign-in button, the hosted login sheet, and a signed-in profile panel. pixillateApp composes the auth + networking graph in one init.
  7. readme + claude.md — short project readme and a CLAUDE.md with architecture / pitfalls / don'ts.

Architecture at a glance

pixillate/
├── Auth/          AuthSession, TokenStore, PKCE, PixivOAuthClient, LoginWebView
├── Networking/    PixivAPIClient, PixivEndpoint, URLSessionProtocol, UserAgent
├── Models/        PixivUser, PixivTokenResponse, PixivTokens
├── Storage/       KeychainStore + TokenPersisting
├── Errors/        PixivError
├── ContentView.swift
└── pixillateApp.swift

Views only ever touch AuthSession. They never see URLSession, Keychain, actors, or tokens directly.

Design decisions worth a review

  • Default MainActor + explicit nonisolated helpers. The project is configured with SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, so everything that does networking, crypto, keychain, or persistence work is explicitly nonisolated to keep it off main.
  • Single-flight refresh. TokenStore.runRefresh() stores the in-flight Task so concurrent callers all await the same result. Refresh-token rejection (400/401) wipes the Keychain and forces a fresh login.
  • No Info.plist changes required. Pixiv's TLS is standard; we don't register the pixiv:// scheme because we intercept the redirect inside the WKWebView rather than letting the OS route it.
  • Test seams ready, no tests yet. URLSessionProtocol and TokenPersisting + InMemoryTokenStore are in place so tests can be added without refactoring.

Test plan

  • Build on an iOS 26.2+ simulator.
  • Tap Sign in with Pixiv → hosted login page appears in the sheet.
  • Complete login → sheet dismisses, signed-in view shows name / @account / user id.
  • Force-quit and relaunch → land directly on the signed-in view (proves Keychain round-trip + bootstrap).
  • Tap Sign out → back to the signed-out view; relaunch stays signed out.
  • Cancel button on the login sheet returns cleanly to the signed-out state.

Known PoC non-goals

No image loading, illust/ranking/bookmark/search endpoints, pagination, proxy config, account switching, biometric gate, or localization yet. CLAUDE.md documents the extension points so future PRs slot in cleanly.

Merge request reports

Loading