Complete re-validation remediation: KMP architecture, iOS platform, XCUITest rewrite

Phases 1-6 of fixes.md — closes all 13 issues from codex_issues_2.md re-validation:

KMP Architecture:
- Fix subscription purchase/restore response contract (VerificationResponse aligned)
- Add feature benefits auth token + APILayer init flow
- Remove ResidenceFormScreen direct API bypass (use APILayer)
- Wire paywall purchase/restore to real SubscriptionApi calls

iOS Platform:
- Add iOS Keychain token storage via Swift KeychainHelper
- Implement Google Sign-In via ASWebAuthenticationSession (GoogleSignInManager)
- DocumentViewModelWrapper observes DataManager for auto-updates
- Add missing accessibility identifiers (document, task columns, Google Sign-In)

XCUITest Rewrite:
- Rewrite test infrastructure: zero sleep() calls, accessibility ID lookups
- Create AuthCriticalPathTests and NavigationCriticalPathTests
- Delete 14 legacy brittle test files (Suite0-10, templates)
- Fix CaseraTests module import (@testable import Casera)

All platforms build clean. TEST BUILD SUCCEEDED.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-18 18:50:13 -06:00
parent 7444f73b46
commit 5e3596db77
47 changed files with 982 additions and 6075 deletions

View File

@@ -0,0 +1,171 @@
import Foundation
import AuthenticationServices
import ComposeApp
/// Handles Google OAuth flow using ASWebAuthenticationSession.
/// Obtains a Google ID token, then sends it to the backend via APILayer.
@MainActor
final class GoogleSignInManager: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding {
static let shared = GoogleSignInManager()
@Published var isLoading = false
@Published var errorMessage: String?
/// Called on successful sign-in with (isVerified: Bool)
var onSignInSuccess: ((Bool) -> Void)?
private var webAuthSession: ASWebAuthenticationSession?
// MARK: - Public
func signIn() {
guard !isLoading else { return }
let clientId = ApiConfig.shared.GOOGLE_WEB_CLIENT_ID
guard ApiConfig.shared.isGoogleSignInConfigured else {
errorMessage = "Google Sign-In is not configured. A Google Cloud client ID is required."
return
}
isLoading = true
errorMessage = nil
// Build Google OAuth URL
let redirectScheme = "com.tt.casera"
let redirectURI = "\(redirectScheme):/oauth2callback"
var components = URLComponents(string: "https://accounts.google.com/o/oauth2/v2/auth")!
components.queryItems = [
URLQueryItem(name: "client_id", value: clientId),
URLQueryItem(name: "redirect_uri", value: redirectURI),
URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "scope", value: "openid email profile"),
URLQueryItem(name: "access_type", value: "offline"),
URLQueryItem(name: "prompt", value: "select_account"),
]
guard let authURL = components.url else {
isLoading = false
errorMessage = "Failed to build authentication URL"
return
}
let session = ASWebAuthenticationSession(
url: authURL,
callbackURLScheme: redirectScheme
) { [weak self] callbackURL, error in
Task { @MainActor in
guard let self else { return }
if let error {
self.isLoading = false
// Don't show error for user cancellation
if (error as NSError).code != ASWebAuthenticationSessionError.canceledLogin.rawValue {
self.errorMessage = "Sign in failed: \(error.localizedDescription)"
}
return
}
guard let callbackURL,
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let code = components.queryItems?.first(where: { $0.name == "code" })?.value else {
self.isLoading = false
self.errorMessage = "Failed to get authorization code from Google"
return
}
await self.exchangeCodeForToken(code: code, redirectURI: redirectURI, clientId: clientId)
}
}
session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = false
webAuthSession = session
session.start()
}
// MARK: - ASWebAuthenticationPresentationContextProviding
nonisolated func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
// Return the key window for presentation
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene
return windowScene?.windows.first(where: { $0.isKeyWindow }) ?? ASPresentationAnchor()
}
// MARK: - Private
/// Exchange authorization code for ID token via Google's token endpoint
private func exchangeCodeForToken(code: String, redirectURI: String, clientId: String) async {
let tokenURL = URL(string: "https://oauth2.googleapis.com/token")!
var request = URLRequest(url: tokenURL)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let body = [
"code": code,
"client_id": clientId,
"redirect_uri": redirectURI,
"grant_type": "authorization_code",
]
request.httpBody = body
.map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? $0.value)" }
.joined(separator: "&")
.data(using: .utf8)
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
isLoading = false
errorMessage = "Failed to exchange authorization code"
return
}
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let idToken = json["id_token"] as? String else {
isLoading = false
errorMessage = "Failed to get ID token from Google"
return
}
// Send ID token to backend
await sendToBackend(idToken: idToken)
} catch {
isLoading = false
errorMessage = "Network error: \(error.localizedDescription)"
}
}
/// Send Google ID token to backend for verification and authentication
private func sendToBackend(idToken: String) async {
let request = GoogleSignInRequest(idToken: idToken)
let result = try? await APILayer.shared.googleSignIn(request: request)
guard let result else {
isLoading = false
errorMessage = "Sign in failed. Please try again."
return
}
if let success = result as? ApiResultSuccess<GoogleSignInResponse>, let response = success.data {
isLoading = false
// Share token and API URL with widget extension
WidgetDataManager.shared.saveAuthToken(response.token)
WidgetDataManager.shared.saveAPIBaseURL(ApiClient.shared.getBaseUrl())
// Track Google Sign In
AnalyticsManager.shared.track(.userSignedInGoogle(isNewUser: response.isNewUser))
// Call success callback with verification status
onSignInSuccess?(response.user.verified)
} else if let error = ApiResultBridge.error(from: result) {
isLoading = false
errorMessage = ErrorMessageParser.parse(error.message)
} else {
isLoading = false
errorMessage = "Sign in failed. Please try again."
}
}
}