import Foundation /// Mirrors the `user` object returned by `GET /api/auth/session` and /// `POST /api/auth/login` (src/app/api/auth/session/route.ts). Field names /// are already camelCase in the JSON, so no custom `CodingKeys`/key-decoding /// strategy is needed anywhere in this file. /// /// Date fields are kept as raw ISO-8601 strings (with fractional seconds, /// e.g. `2026-08-20T19:37:42.307Z` from `Date.toISOString()`) rather than /// `Date`, because `JSONDecoder`'s built-in `.iso8601` strategy does NOT /// parse fractional seconds and would silently fail to decode every /// timestamp this backend sends. Use `ISO8601.parse(_:)` (Support/ISO8601.swift) /// where you actually need a `Date`. struct User: Decodable, Identifiable, Equatable { let id: String let email: String? let name: String? let plan: String let isGuest: Bool let isPro: Bool let cancelAtPeriodEnd: Bool let emailVerified: Bool let launchBonus: Bool let freeScanAllowance: Int let scanCount: Int let company: String? let useCase: String? let expiresAt: String? let createdAt: String? let onboardingCompletedAt: String? /// True once onboarding (company/use-case/etc.) has been completed — the /// dashboard's post-signup wizard on web, skippable but tracked the same /// way in the app. var hasCompletedOnboarding: Bool { onboardingCompletedAt != nil } /// Reconstructs the UUID `newId("usr")` originally minted this account's /// `id` from (see `src/lib/auth/tokens.ts` on the backend: `"usr_" + /// randomUUID().replace(/-/g, "")` — i.e. `id` IS a UUID with its dashes /// stripped and a prefix glued on, nothing more). Re-inserting the /// dashes recovers that exact UUID losslessly — no extra network call, /// no server-issued token to fetch and cache. /// /// Passed as StoreKit's `appAccountToken` on purchase (see /// `StoreKitPurchaseService.purchase`) so `POST /api/webhooks/apple` /// (`src/lib/billing/appleIAP.ts` → `userIdFromAppAccountToken`) can /// reverse the same transformation server-side and know which account a /// given App Store transaction belongs to — that's the ONLY thing this /// value is for; it carries no other meaning to Apple. var appleAccountToken: UUID? { guard id.hasPrefix("usr_") else { return nil } let hex = String(id.dropFirst(4)) guard hex.count == 32, hex.allSatisfy(\.isHexDigit) else { return nil } let parts = [ hex.prefix(8), hex.dropFirst(8).prefix(4), hex.dropFirst(12).prefix(4), hex.dropFirst(16).prefix(4), hex.dropFirst(20), ] return UUID(uuidString: parts.joined(separator: "-")) } } /// The minimal user echo returned inline by `POST /api/auth/login` and /// `/signup` (a subset of `User` — those endpoints don't run the extra /// queries `/api/auth/session` does). Kept separate so a login response /// decodes without requiring fields it doesn't send. struct LoginResponse: Decodable { let status: String let user: LoginUser /// Present only when the request carried `X-Client: ios` — see /// src/app/api/auth/login/route.ts. Absent for a browser-style caller. let token: String? let expiresAt: String? } struct LoginUser: Decodable { let id: String let email: String? let name: String? let plan: String let onboardingCompletedAt: String? } struct SessionResponse: Decodable { let user: User? }