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:
@@ -0,0 +1,33 @@
|
||||
package com.example.casera.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import com.example.casera.network.APILayer
|
||||
import com.example.casera.ui.subscription.UpgradeScreen
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* iOS: Purchase flow is handled in Swift via StoreKitManager.
|
||||
* Restore calls backend to refresh subscription status.
|
||||
*/
|
||||
@Composable
|
||||
actual fun PlatformUpgradeScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onSubscriptionChanged: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
UpgradeScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onPurchase = { _ ->
|
||||
// iOS purchase flow is handled by StoreKitManager in Swift layer
|
||||
onNavigateBack()
|
||||
},
|
||||
onRestorePurchases = {
|
||||
scope.launch {
|
||||
APILayer.getSubscriptionStatus(forceRefresh = true)
|
||||
onSubscriptionChanged()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -3,33 +3,71 @@ package com.example.casera.storage
|
||||
import platform.Foundation.NSUserDefaults
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
/**
|
||||
* Protocol for iOS Keychain operations. Implemented in Swift (KeychainHelper)
|
||||
* and injected before DataManager initialization.
|
||||
*
|
||||
* Kotlin/Native cannot directly use the Security framework (SecItem* APIs)
|
||||
* because CFStringRef keys like kSecClass don't bridge to NSCopying.
|
||||
*/
|
||||
interface KeychainDelegate {
|
||||
fun save(key: String, value: String): Boolean
|
||||
fun get(key: String): String?
|
||||
fun delete(key: String): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* iOS implementation of TokenManager.
|
||||
*
|
||||
* SECURITY NOTE: Currently uses NSUserDefaults for token storage.
|
||||
* For production hardening, migrate to iOS Keychain via a Swift helper
|
||||
* exposed to KMP through an expect/actual boundary or SKIE bridge.
|
||||
* NSUserDefaults is not encrypted and should not store long-lived auth tokens
|
||||
* in apps handling sensitive data.
|
||||
* Uses iOS Keychain via [KeychainDelegate] for secure token storage.
|
||||
* Falls back to NSUserDefaults if delegate is not set (should not happen
|
||||
* in production — delegate is set in iOSApp.init before DataManager init).
|
||||
*
|
||||
* Migration plan:
|
||||
* 1. Create a Swift KeychainHelper class with save/get/delete methods
|
||||
* 2. Expose it to Kotlin via SKIE or a protocol-based expect/actual
|
||||
* 3. Use service "com.tt.casera", account "auth_token"
|
||||
* On first read, migrates any existing NSUserDefaults token to Keychain.
|
||||
*/
|
||||
actual class TokenManager {
|
||||
private val prefs = NSUserDefaults.standardUserDefaults
|
||||
|
||||
actual fun saveToken(token: String) {
|
||||
prefs.setObject(token, forKey = TOKEN_KEY)
|
||||
prefs.synchronize()
|
||||
val delegate = keychainDelegate
|
||||
if (delegate != null) {
|
||||
delegate.save(TOKEN_KEY, token)
|
||||
// Clean up old NSUserDefaults entry if it exists
|
||||
prefs.removeObjectForKey(TOKEN_KEY)
|
||||
prefs.synchronize()
|
||||
} else {
|
||||
// Fallback (should not happen in production)
|
||||
prefs.setObject(token, forKey = TOKEN_KEY)
|
||||
prefs.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
actual fun getToken(): String? {
|
||||
val delegate = keychainDelegate
|
||||
|
||||
// Try Keychain first
|
||||
if (delegate != null) {
|
||||
val keychainToken = delegate.get(TOKEN_KEY)
|
||||
if (keychainToken != null) return keychainToken
|
||||
|
||||
// Check NSUserDefaults for migration
|
||||
val oldToken = prefs.stringForKey(TOKEN_KEY)
|
||||
if (oldToken != null) {
|
||||
// Migrate to Keychain
|
||||
delegate.save(TOKEN_KEY, oldToken)
|
||||
prefs.removeObjectForKey(TOKEN_KEY)
|
||||
prefs.synchronize()
|
||||
return oldToken
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Fallback to NSUserDefaults (should not happen in production)
|
||||
return prefs.stringForKey(TOKEN_KEY)
|
||||
}
|
||||
|
||||
actual fun clearToken() {
|
||||
keychainDelegate?.delete(TOKEN_KEY)
|
||||
prefs.removeObjectForKey(TOKEN_KEY)
|
||||
prefs.synchronize()
|
||||
}
|
||||
@@ -37,6 +75,12 @@ actual class TokenManager {
|
||||
companion object {
|
||||
private const val TOKEN_KEY = "auth_token"
|
||||
|
||||
/**
|
||||
* Set from Swift in iOSApp.init() BEFORE DataManager.initialize().
|
||||
* This enables Keychain storage for auth tokens.
|
||||
*/
|
||||
var keychainDelegate: KeychainDelegate? = null
|
||||
|
||||
@Volatile
|
||||
private var instance: TokenManager? = null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user