feat: add iOS support and harden receipt scanning
This commit is contained in:
95
app/ios/ScanReceipts/Networking/AuthAPI.swift
Normal file
95
app/ios/ScanReceipts/Networking/AuthAPI.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
import Foundation
|
||||
|
||||
/// Thin wrapper around the `/api/auth/*` endpoints. `AppState` is the only
|
||||
/// caller that should hold state derived from these — feature screens call
|
||||
/// through `AppState`, not this type directly, so there is exactly one place
|
||||
/// that decides what "signed in" means.
|
||||
enum AuthAPI {
|
||||
struct SignupBody: Encodable {
|
||||
let name: String?
|
||||
let email: String
|
||||
let password: String
|
||||
let lang: String
|
||||
}
|
||||
|
||||
struct SignupResponse: Decodable {
|
||||
let status: String
|
||||
/// Only ever present against a local dev backend with no SMTP
|
||||
/// configured (see src/app/api/auth/signup/route.ts) — never in
|
||||
/// production. Useful for the simulator during development.
|
||||
let devLink: String?
|
||||
}
|
||||
|
||||
struct LoginBody: Encodable {
|
||||
let email: String
|
||||
let password: String
|
||||
let remember: Bool
|
||||
}
|
||||
|
||||
struct AppleSignInBody: Encodable {
|
||||
let identityToken: String
|
||||
let fullName: String?
|
||||
}
|
||||
|
||||
/// `POST /api/auth/apple` — native Sign in with Apple. `identityToken` is
|
||||
/// `ASAuthorizationAppleIDCredential.identityToken` decoded to a UTF-8
|
||||
/// string; `fullName` is `credential.fullName` formatted for display,
|
||||
/// which Apple only ever supplies on the user's FIRST authorization with
|
||||
/// this app (pass `nil` on every subsequent sign-in — there is nothing to
|
||||
/// send). The backend verifies the token itself; nothing here is trusted
|
||||
/// data, it's just what gets forwarded for verification.
|
||||
static func appleSignIn(identityToken: String, fullName: String?) async throws -> LoginResponse {
|
||||
let request = try APIRequest.json(
|
||||
path: "/api/auth/apple",
|
||||
method: .post,
|
||||
body: AppleSignInBody(identityToken: identityToken, fullName: fullName),
|
||||
requiresAuth: false
|
||||
)
|
||||
return try await APIClient.shared.send(request)
|
||||
}
|
||||
|
||||
/// `POST /api/auth/signup`. No session is created — the account is inert
|
||||
/// until the emailed confirmation link is opened, exactly like the web
|
||||
/// flow. The response is deliberately neutral (see the route's doc
|
||||
/// comment): callers cannot tell a new signup apart from "already
|
||||
/// registered" from this response alone.
|
||||
static func signup(name: String?, email: String, password: String) async throws -> SignupResponse {
|
||||
let request = try APIRequest.json(
|
||||
path: "/api/auth/signup",
|
||||
method: .post,
|
||||
body: SignupBody(name: name, email: email, password: password, lang: "de"),
|
||||
requiresAuth: false
|
||||
)
|
||||
return try await APIClient.shared.send(request)
|
||||
}
|
||||
|
||||
/// `POST /api/auth/login`. On success the backend returns the raw session
|
||||
/// token in the body (because the request carries `X-Client: ios` — see
|
||||
/// APIClient) instead of only a Set-Cookie header. The caller is
|
||||
/// responsible for persisting it via `KeychainTokenStore`.
|
||||
static func login(email: String, password: String, remember: Bool = true) async throws -> LoginResponse {
|
||||
let request = try APIRequest.json(
|
||||
path: "/api/auth/login",
|
||||
method: .post,
|
||||
body: LoginBody(email: email, password: password, remember: remember),
|
||||
requiresAuth: false
|
||||
)
|
||||
return try await APIClient.shared.send(request)
|
||||
}
|
||||
|
||||
/// `GET /api/auth/session` — "who am I". Never throws for "signed out";
|
||||
/// that's `{ user: null }`, HTTP 200.
|
||||
static func session() async throws -> SessionResponse {
|
||||
try await APIClient.shared.send(APIRequest(path: "/api/auth/session", method: .get))
|
||||
}
|
||||
|
||||
/// `POST /api/auth/logout`. Best-effort: the caller should clear the
|
||||
/// local token and navigate to the login screen regardless of whether
|
||||
/// this succeeds (mirrors the web route's own "logout always proceeds"
|
||||
/// philosophy).
|
||||
static func logout() async throws {
|
||||
try await APIClient.shared.sendDiscardingResponse(
|
||||
APIRequest(path: "/api/auth/logout", method: .post)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user