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

@@ -1,6 +1,7 @@
import Foundation
import ComposeApp
import SwiftUI
import Combine
// MARK: - Architecture Note
//
@@ -17,7 +18,7 @@ import SwiftUI
// - Used by detail views (DocumentDetailView, EditDocumentView)
// - Manages explicit state types (Loading/Success/Error) for single-document operations
// - Loads individual document detail, handles update and delete with state feedback
// - Does NOT observe DataManager -- loads fresh data per-request via APILayer
// - Observes DataManager for automatic detail updates after mutations
// - Uses protocol-based state enums for SwiftUI view branching
//
// Both call through APILayer (which updates DataManager), so list views
@@ -70,6 +71,7 @@ struct DeleteImageStateError: DeleteImageState {
let message: String
}
@MainActor
class DocumentViewModelWrapper: ObservableObject {
@Published var documentsState: DocumentState = DocumentStateIdle()
@Published var documentDetailState: DocumentDetailState = DocumentDetailStateIdle()
@@ -77,6 +79,25 @@ class DocumentViewModelWrapper: ObservableObject {
@Published var deleteState: DeleteState = DeleteStateIdle()
@Published var deleteImageState: DeleteImageState = DeleteImageStateIdle()
/// The document ID currently loaded in detail view, used for auto-update
private var loadedDocumentId: Int32?
private var cancellables = Set<AnyCancellable>()
init() {
// Observe DataManager documents for auto-update of loaded detail
DataManagerObservable.shared.$documents
.receive(on: DispatchQueue.main)
.sink { [weak self] documents in
guard let self, let docId = self.loadedDocumentId else { return }
// Only auto-update if we're in a success state
guard self.documentDetailState is DocumentDetailStateSuccess else { return }
if let updated = documents.first(where: { $0.id?.int32Value == docId }) {
self.documentDetailState = DocumentDetailStateSuccess(document: updated)
}
}
.store(in: &cancellables)
}
func loadDocuments(
residenceId: Int32? = nil,
documentType: String? = nil,
@@ -87,9 +108,7 @@ class DocumentViewModelWrapper: ObservableObject {
tags: String? = nil,
search: String? = nil
) {
DispatchQueue.main.async {
self.documentsState = DocumentStateLoading()
}
self.documentsState = DocumentStateLoading()
Task {
do {
@@ -105,7 +124,7 @@ class DocumentViewModelWrapper: ObservableObject {
forceRefresh: false
)
await MainActor.run {
do {
if let success = result as? ApiResultSuccess<NSArray> {
let documents = success.data as? [Document] ?? []
self.documentsState = DocumentStateSuccess(documents: documents)
@@ -116,7 +135,7 @@ class DocumentViewModelWrapper: ObservableObject {
}
}
} catch {
await MainActor.run {
do {
self.documentsState = DocumentStateError(message: error.localizedDescription)
}
}
@@ -124,15 +143,14 @@ class DocumentViewModelWrapper: ObservableObject {
}
func loadDocumentDetail(id: Int32) {
DispatchQueue.main.async {
self.documentDetailState = DocumentDetailStateLoading()
}
loadedDocumentId = id
self.documentDetailState = DocumentDetailStateLoading()
Task {
do {
let result = try await APILayer.shared.getDocument(id: id, forceRefresh: false)
await MainActor.run {
do {
if let success = result as? ApiResultSuccess<Document>, let document = success.data {
self.documentDetailState = DocumentDetailStateSuccess(document: document)
} else if let error = ApiResultBridge.error(from: result) {
@@ -142,7 +160,7 @@ class DocumentViewModelWrapper: ObservableObject {
}
}
} catch {
await MainActor.run {
do {
self.documentDetailState = DocumentDetailStateError(message: error.localizedDescription)
}
}
@@ -170,9 +188,7 @@ class DocumentViewModelWrapper: ObservableObject {
startDate: String? = nil,
endDate: String? = nil
) {
DispatchQueue.main.async {
self.updateState = UpdateStateLoading()
}
self.updateState = UpdateStateLoading()
Task {
do {
@@ -199,7 +215,7 @@ class DocumentViewModelWrapper: ObservableObject {
endDate: endDate
)
await MainActor.run {
do {
if let success = result as? ApiResultSuccess<Document>, let document = success.data {
self.updateState = UpdateStateSuccess(document: document)
// Also refresh the detail state
@@ -211,7 +227,7 @@ class DocumentViewModelWrapper: ObservableObject {
}
}
} catch {
await MainActor.run {
do {
self.updateState = UpdateStateError(message: error.localizedDescription)
}
}
@@ -219,15 +235,13 @@ class DocumentViewModelWrapper: ObservableObject {
}
func deleteDocument(id: Int32) {
DispatchQueue.main.async {
self.deleteState = DeleteStateLoading()
}
self.deleteState = DeleteStateLoading()
Task {
do {
let result = try await APILayer.shared.deleteDocument(id: id)
await MainActor.run {
do {
if result is ApiResultSuccess<KotlinUnit> {
self.deleteState = DeleteStateSuccess()
} else if let error = ApiResultBridge.error(from: result) {
@@ -237,7 +251,7 @@ class DocumentViewModelWrapper: ObservableObject {
}
}
} catch {
await MainActor.run {
do {
self.deleteState = DeleteStateError(message: error.localizedDescription)
}
}
@@ -245,27 +259,21 @@ class DocumentViewModelWrapper: ObservableObject {
}
func resetUpdateState() {
DispatchQueue.main.async {
self.updateState = UpdateStateIdle()
}
self.updateState = UpdateStateIdle()
}
func resetDeleteState() {
DispatchQueue.main.async {
self.deleteState = DeleteStateIdle()
}
self.deleteState = DeleteStateIdle()
}
func deleteDocumentImage(documentId: Int32, imageId: Int32) {
DispatchQueue.main.async {
self.deleteImageState = DeleteImageStateLoading()
}
self.deleteImageState = DeleteImageStateLoading()
Task {
do {
let result = try await APILayer.shared.deleteDocumentImage(documentId: documentId, imageId: imageId)
await MainActor.run {
do {
if let success = result as? ApiResultSuccess<Document>, let document = success.data {
self.deleteImageState = DeleteImageStateSuccess()
// Refresh detail state with updated document (image removed)
@@ -277,7 +285,7 @@ class DocumentViewModelWrapper: ObservableObject {
}
}
} catch {
await MainActor.run {
do {
self.deleteImageState = DeleteImageStateError(message: error.localizedDescription)
}
}
@@ -285,8 +293,6 @@ class DocumentViewModelWrapper: ObservableObject {
}
func resetDeleteImageState() {
DispatchQueue.main.async {
self.deleteImageState = DeleteImageStateIdle()
}
self.deleteImageState = DeleteImageStateIdle()
}
}