sign in with pixiv (oauth + api layer scaffolding)
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:
-
foundation —
PixivError, DTOs (PixivUser,PixivTokenResponse,PixivTokens),UserAgent,URLSessionProtocoltest seam. -
pkce + oauth token exchange — PKCE (S256) and the HTTP side of
/auth/tokenfor bothauthorization_codeandrefresh_tokengrants. -
keychain persistence + token store actor —
KeychainStore(generic-password blob +TokenPersistingprotocol) and theTokenStoreactor.TokenStoreruns single-flight refresh so concurrent API callers share one round-trip. -
authed api client —
PixivAPIClient.send<T>with transparent refresh-on-401 retry, fed by a declarativePixivEndpointenum. -
login webview —
LoginWebView(UIViewRepresentable +WKNavigationDelegate) catches Pixiv'spixiv://account/login?code=...redirect and extracts the OAuth code.ASWebAuthenticationSessionisn't viable here — its callback matcher only fires for URL schemes the app owns. -
auth session facade + sign-in/out UI —
@Observable AuthSessionis the whole view-facing contract.ContentViewswitches onauth.statebetween a sign-in button, the hosted login sheet, and a signed-in profile panel.pixillateAppcomposes the auth + networking graph in one init. - 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
nonisolatedhelpers. The project is configured withSWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, so everything that does networking, crypto, keychain, or persistence work is explicitlynonisolatedto keep it off main. -
Single-flight refresh.
TokenStore.runRefresh()stores the in-flightTaskso 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.
URLSessionProtocolandTokenPersisting+InMemoryTokenStoreare 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.