Manos Chorianopoulos

network fixes, SQLite Raw SQL migration, and many more. open framework_migration…

…_changelog for details
Showing 36 changed files with 1041 additions and 58 deletions
# macOS system files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Swift Package Manager
.build/
.swiftpm/
Package.resolved
# Xcode build files
DerivedData/
*.xcworkspace/xcuserdata/
*.xcodeproj/xcuserdata/
*.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
*.xcodeproj/xcshareddata/IDEWorkspaceChecks.plist
# CocoaPods (root level only - framework has its own Pods)
/Pods/
/Podfile.lock
# Build artifacts
build/
*.ipa
*.dSYM.zip
*.dSYM
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Temporary files
*.tmp
*.temp
.tmp/
......
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>SwiftWarplyFramework.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>SwiftWarplyFramework</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>
This diff is collapsed. Click to expand it.
# DatabaseManager Debug Analysis - UPDATED
## 🎉 **SOLUTION FOUND - SQLite.swift 0.12.2 Works!**
**Date:** December 26, 2024
**Status:****DATABASE ISSUES RESOLVED**
**SQLite.swift Version:** 0.12.2 (downgraded from 0.14/0.15)
**Swift Version:** 5.0
---
## 🚨 **Original Problem (SOLVED)**
### **Root Cause Identified:**
- **SQLite.swift 0.14+ requires Swift 5.3+**
- **SQLite.swift 0.15+ requires Swift 5.5+**
- **Our Swift 5.0 was incompatible** with newer SQLite.swift versions
### **Original Errors (FIXED):**
```swift
// BEFORE (failing with 0.14/0.15):
Expression<String?>("access_token") // ❌ Type inference failure
Expression<String?>("refresh_token") // ❌ Cannot resolve constructor
// AFTER (working with 0.12.2):
Expression<String?>("access_token") // ✅ Compiles successfully
Expression<String?>("refresh_token") // ✅ Compiles successfully
```
---
## ✅ **SOLUTION IMPLEMENTED**
### **Version Downgrade:**
```swift
// Package.swift - WORKING CONFIGURATION
.package(url: "https://github.com/stephencelis/SQLite.swift", .exact("0.12.2"))
```
### **Why 0.12.2 Works:**
-**Built for Swift 5.0** - perfect compatibility
-**Stable Expression API** - no type inference issues
-**Mature codebase** - fewer breaking changes
-**Production tested** - widely used in Swift 5.0 projects
---
## 📊 **Current Status**
### **✅ RESOLVED - Database Issues:**
-**SQLite.swift compilation** - no more Expression errors
-**DatabaseManager.swift** - compiles successfully
-**Type inference** - Swift 5.0 compatible patterns
-**Expression constructors** - working properly
### **⚠️ REMAINING - Non-Database Issues:**
The following errors are **NOT database-related** and need separate fixes:
#### **1. WarplySDK.swift (8 errors):**
```swift
// Error 1: Missing switch case
switch networkError {
// Missing: case .invalidResponse:
}
// Error 2: Type conversion
"error_code": error.errorCode, // ❌ Int to String conversion needed
// Fix: "error_code": String(error.errorCode),
// Error 3-5: Missing configuration properties
config.enableRequestCaching // ❌ Property doesn't exist
config.analyticsEnabled // ❌ Property doesn't exist
config.crashReportingEnabled // ❌ Property doesn't exist
// Error 6: Missing NetworkService method
networkService.setTokens(...) // ❌ Method doesn't exist
// Error 7-8: Async/await compatibility
networkService.getAccessToken() // ❌ Async call in non-async function
```
#### **2. DatabaseConfiguration.swift (3 errors):**
```swift
// Error 1: Read-only property
resourceValues.fileProtection = dataProtectionClass // ❌ Get-only property
// Error 2: Type mismatch
FileProtectionType vs URLFileProtection? // ❌ Type incompatibility
// Error 3: Immutable URL
let fileURL = URL(...) // ❌ Cannot mutate let constant
// Fix: var fileURL = URL(...)
```
---
## 🎯 **Next Steps - Fix Remaining Issues**
### **Priority 1: WarplySDK.swift Fixes (30 minutes)**
#### **Fix 1: Add Missing Switch Case**
```swift
switch networkError {
case .noConnection:
// existing code
case .timeout:
// existing code
case .invalidResponse: // ← ADD THIS
print("Invalid response received")
// Handle invalid response
}
```
#### **Fix 2: Type Conversion**
```swift
// BEFORE:
"error_code": error.errorCode,
// AFTER:
"error_code": String(error.errorCode),
```
#### **Fix 3: Add Missing Configuration Properties**
```swift
// Add to WarplyNetworkConfig:
public var enableRequestCaching: Bool = false
// Add to WarplyConfiguration:
public var analyticsEnabled: Bool = false
public var crashReportingEnabled: Bool = false
public var autoRegistrationEnabled: Bool = false
```
#### **Fix 4: Add Missing NetworkService Method**
```swift
// Add to NetworkService:
public func setTokens(accessToken: String?, refreshToken: String?) {
// Implementation
}
```
#### **Fix 5: Fix Async/Await Issues**
```swift
// BEFORE:
public func constructCampaignParams(_ campaign: CampaignItemModel) -> String {
"access_token": networkService.getAccessToken() ?? "",
// AFTER:
public func constructCampaignParams(_ campaign: CampaignItemModel) async throws -> String {
"access_token": try await networkService.getAccessToken() ?? "",
```
### **Priority 2: DatabaseConfiguration.swift Fixes (10 minutes)**
#### **Fix 1: File Protection API**
```swift
// BEFORE:
let fileURL = URL(fileURLWithPath: filePath)
var resourceValues = URLResourceValues()
resourceValues.fileProtection = dataProtectionClass
// AFTER:
var fileURL = URL(fileURLWithPath: filePath)
var resourceValues = URLResourceValues()
resourceValues.fileProtection = URLFileProtection(rawValue: dataProtectionClass.rawValue)
try fileURL.setResourceValues(resourceValues)
```
---
## 📋 **Implementation Checklist**
### **✅ COMPLETED:**
- [x] **Identify root cause** - Swift 5.0 vs SQLite.swift version incompatibility
- [x] **Test SQLite.swift 0.12.2** - confirmed working
- [x] **Verify database compilation** - Expression errors resolved
- [x] **Document solution** - version downgrade approach
### **🔄 IN PROGRESS:**
- [ ] **Fix WarplySDK.swift errors** (8 errors)
- [ ] **Fix DatabaseConfiguration.swift errors** (3 errors)
- [ ] **Test full framework compilation**
- [ ] **Verify database operations work**
### **📅 TODO:**
- [ ] **Update Package.swift documentation** - note Swift 5.0 requirement
- [ ] **Add version compatibility notes** - for future developers
- [ ] **Test database operations** - ensure CRUD works
- [ ] **Performance testing** - verify no regressions
---
## 🎯 **Success Metrics**
### **✅ ACHIEVED:**
1. **Database compilation** - SQLite.swift errors eliminated
2. **Version compatibility** - Swift 5.0 + SQLite.swift 0.12.2 working
3. **Expression constructors** - type inference working properly
### **🎯 TARGET (Next 45 minutes):**
1. **Full framework compilation** - all errors resolved
2. **Database operations** - CRUD functionality verified
3. **Integration testing** - NetworkService + DatabaseManager working together
---
## 📈 **Lessons Learned**
### **Key Insights:**
1. **Version compatibility is critical** - newer isn't always better
2. **Swift version constraints** - check library requirements carefully
3. **Type inference evolution** - Swift 5.0 vs 5.3+ differences significant
4. **Downgrading can solve issues** - when newer versions break compatibility
### **Best Practices:**
1. **Lock dependency versions** - use .exact() for critical libraries
2. **Test with target Swift version** - before upgrading dependencies
3. **Separate database from app logic** - isolate compilation issues
4. **Document version requirements** - for future maintenance
---
## 🚀 **Conclusion**
**The core DatabaseManager issue is SOLVED!** 🎉
-**SQLite.swift 0.12.2** works perfectly with Swift 5.0
-**Database compilation** successful
-**Expression constructors** working properly
**Remaining work:** Fix 11 non-database errors in WarplySDK.swift and DatabaseConfiguration.swift (estimated 45 minutes).
**The framework is very close to full compilation success!**
## __Alternative Solutions (If Version Doesn't Work)__
### __Solution A: Syntax Fix with Current Version__
Keep whatever version you have, but fix the Expression patterns:
```swift
class DatabaseManager {
private var db: Connection?
// Define tables as static properties
private static let tokensTable = Table("tokens")
private static let eventsTable = Table("events")
// Define columns with explicit types
private static let tokenId = Expression<Int64>("id")
private static let accessToken = Expression<String?>("access_token")
private static let refreshToken = Expression<String?>("refresh_token")
// Use static references in methods
func createTokensTable() throws {
try db?.run(Self.tokensTable.create(ifNotExists: true) { table in
table.column(Self.tokenId, primaryKey: .autoincrement)
table.column(Self.accessToken)
table.column(Self.refreshToken)
})
}
}
```
### __Solution B: Raw SQL Approach__
Use SQLite.swift's raw SQL capabilities instead of Expression builders:
```swift
func saveToken(_ token: TokenModel) throws {
let sql = """
INSERT OR REPLACE INTO tokens
(access_token, refresh_token, client_id, client_secret, expires_at)
VALUES (?, ?, ?, ?, ?)
"""
try db?.execute(sql, token.accessToken, token.refreshToken,
token.clientId, token.clientSecret, token.expiresAt)
}
func getToken() throws -> TokenModel? {
let sql = "SELECT * FROM tokens LIMIT 1"
for row in try db?.prepare(sql) ?? [] {
return TokenModel(
accessToken: row[0] as? String,
refreshToken: row[1] as? String,
clientId: row[2] as? String,
clientSecret: row[3] as? String,
expiresAt: row[4] as? Date
)
}
return nil
}
```
### __Solution C: Hybrid Approach__
Use SQLite.swift for connections, raw SQL for operations:
```swift
class DatabaseManager {
private var db: Connection?
init(databasePath: String) throws {
db = try Connection(databasePath)
try createTables()
}
private func createTables() throws {
// Use raw SQL for table creation
try db?.execute("""
CREATE TABLE IF NOT EXISTS tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
access_token TEXT,
refresh_token TEXT,
client_id TEXT,
client_secret TEXT,
expires_at REAL
)
""")
}
}
```
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
......@@ -3,16 +3,34 @@
{
"identity" : "rsbarcodes_swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/yeahdongcn/RSBarcodes_Swift.git",
"location" : "https://github.com/yeahdongcn/RSBarcodes_Swift",
"state" : {
"revision" : "241de72a96f49b1545d5de3c00fae170c2675c41",
"version" : "5.2.0"
}
},
{
"identity" : "sqlite.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/stephencelis/SQLite.swift",
"state" : {
"revision" : "392dd6058624d9f6c5b4c769d165ddd8c7293394",
"version" : "0.15.4"
}
},
{
"identity" : "swift-toolchain-sqlite",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-toolchain-sqlite",
"state" : {
"revision" : "b626d3002773b1a1304166643e7f118f724b2132",
"version" : "1.0.4"
}
},
{
"identity" : "swifteventbus",
"kind" : "remoteSourceControl",
"location" : "https://github.com/cesarferreira/SwiftEventBus.git",
"location" : "https://github.com/cesarferreira/SwiftEventBus",
"state" : {
"revision" : "a30ff35e616f507d8a8d122dac32a2150371a87e",
"version" : "5.1.0"
......
......@@ -21,14 +21,16 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/yeahdongcn/RSBarcodes_Swift", from: "5.2.0"),
.package(url: "https://github.com/cesarferreira/SwiftEventBus", from: "5.0.0")
.package(url: "https://github.com/cesarferreira/SwiftEventBus", from: "5.0.0"),
.package(url: "https://github.com/stephencelis/SQLite.swift", exact: "0.12.2")
],
targets: [
.target(
name: "SwiftWarplyFramework",
dependencies: [
.product(name: "RSBarcodes_Swift", package: "RSBarcodes_Swift"),
.product(name: "SwiftEventBus", package: "SwiftEventBus")
.product(name: "SwiftEventBus", package: "SwiftEventBus"),
.product(name: "SQLite", package: "SQLite.swift")
],
path: "SwiftWarplyFramework/SwiftWarplyFramework",
exclude: [
......
......@@ -49,6 +49,7 @@ Pod::Spec.new do |spec|
spec.dependency 'RSBarcodes_Swift', '~> 5.2.0'
# spec.dependency 'RSBarcodes_Swift', '~> 5.1.1'
spec.dependency 'SwiftEventBus'
spec.dependency 'SQLite.swift', '~> 0.12.2'
# spec.resource_bundles = { 'ResourcesBundle' => ['SwiftWarplyFramework/**/*.{png,jpeg,jpg,storyboard,xib,xcassets,json,ttf,imageset,strings}'] }
......
{
"originHash" : "12dce73308b76580a096b2ddc2db953ca534c29f52e5b13e15c81719afbc8e45",
"pins" : [
{
"identity" : "rsbarcodes_swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/yeahdongcn/RSBarcodes_Swift",
"state" : {
"revision" : "241de72a96f49b1545d5de3c00fae170c2675c41",
"version" : "5.2.0"
}
},
{
"identity" : "swifteventbus",
"kind" : "remoteSourceControl",
"location" : "https://github.com/cesarferreira/SwiftEventBus",
"state" : {
"revision" : "a30ff35e616f507d8a8d122dac32a2150371a87e",
"version" : "5.1.0"
}
}
],
"version" : 3
}
{
"originHash" : "17e77d02482a9bad5f5e4730583b6ef8e884bc07c7c794430f8edee2618193bc",
"originHash" : "cb944cd3bee35f5e65fbd247311810bd6adc1d6454816597431789e670c31595",
"pins" : [
{
"identity" : "rsbarcodes_swift",
......@@ -11,6 +11,15 @@
}
},
{
"identity" : "sqlite.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/stephencelis/SQLite.swift",
"state" : {
"revision" : "0a9893ec030501a3956bee572d6b4fdd3ae158a1",
"version" : "0.12.2"
}
},
{
"identity" : "swifteventbus",
"kind" : "remoteSourceControl",
"location" : "https://github.com/cesarferreira/SwiftEventBus",
......
//
// KeychainManager.swift
// SwiftWarplyFramework
//
// Created by Warply on 25/6/25.
//
import Foundation
import Security
/// Errors that can occur during Keychain operations
enum KeychainError: Error, LocalizedError {
case keyGenerationFailed
case keyNotFound
case storageError(OSStatus)
case retrievalError(OSStatus)
case deletionError(OSStatus)
case invalidKeyData
case bundleIdNotAvailable
var errorDescription: String? {
switch self {
case .keyGenerationFailed:
return "Failed to generate encryption key using SecRandomCopyBytes"
case .keyNotFound:
return "Encryption key not found in Keychain"
case .storageError(let status):
return "Failed to store key in Keychain: \(status) (\(SecCopyErrorMessageString(status, nil) ?? "Unknown error" as CFString))"
case .retrievalError(let status):
return "Failed to retrieve key from Keychain: \(status) (\(SecCopyErrorMessageString(status, nil) ?? "Unknown error" as CFString))"
case .deletionError(let status):
return "Failed to delete key from Keychain: \(status) (\(SecCopyErrorMessageString(status, nil) ?? "Unknown error" as CFString))"
case .invalidKeyData:
return "Invalid key data format - expected 32 bytes for AES-256"
case .bundleIdNotAvailable:
return "Bundle identifier not available - required for Keychain isolation"
}
}
}
/// Thread-safe manager for secure encryption key storage using iOS Keychain Services
/// Provides automatic key generation and Bundle ID-based isolation between client apps
actor KeychainManager {
/// Shared singleton instance
static let shared = KeychainManager()
/// Private initializer to enforce singleton pattern
private init() {}
// MARK: - Bundle ID-Based Isolation
/// Unique Keychain service identifier based on client app's Bundle ID
/// This ensures complete isolation between different client apps using the framework
private var keychainService: String {
guard let bundleId = Bundle.main.bundleIdentifier, !bundleId.isEmpty else {
// Use fallback for edge cases, but log the issue
print("⚠️ [KeychainManager] Bundle ID not available, using fallback identifier")
return "com.warply.sdk.unknown"
}
return "com.warply.sdk.\(bundleId)"
}
/// Simple account identifier for the database encryption key
/// Isolation is provided by the service identifier above
private let databaseKeyIdentifier = "database_encryption_key"
/// Base Keychain query dictionary with security attributes
private var keychainQuery: [String: Any] {
return [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: keychainService,
kSecAttrAccount as String: databaseKeyIdentifier,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
}
// MARK: - Public API
/// Gets existing database encryption key or creates a new one if none exists
/// This is the main entry point for database encryption key management
/// - Returns: 256-bit AES encryption key
/// - Throws: KeychainError if key generation or storage fails
func getOrCreateDatabaseKey() async throws -> Data {
print("🔑 [KeychainManager] Requesting database encryption key for app: \(Bundle.main.bundleIdentifier ?? "unknown")")
// Try to get existing key first
do {
let existingKey = try await getExistingDatabaseKey()
print("✅ [KeychainManager] Retrieved existing database encryption key")
return existingKey
} catch KeychainError.keyNotFound {
// Generate new key if none exists
print("🔑 [KeychainManager] No existing key found, generating new database encryption key")
let newKey = try generateEncryptionKey()
try await storeDatabaseKey(newKey)
print("✅ [KeychainManager] Generated and stored new database encryption key")
return newKey
}
// Re-throw other errors
}
/// Checks if a database encryption key exists in the Keychain
/// - Returns: true if key exists, false otherwise
func keyExists() async -> Bool {
do {
_ = try await getExistingDatabaseKey()
return true
} catch {
return false
}
}
/// Deletes the database encryption key from the Keychain
/// This will make all encrypted data unreadable
/// - Throws: KeychainError if deletion fails
func deleteDatabaseKey() async throws {
print("🗑️ [KeychainManager] Deleting database encryption key for app: \(Bundle.main.bundleIdentifier ?? "unknown")")
let query = keychainQuery
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
print("❌ [KeychainManager] Failed to delete database key: \(status)")
throw KeychainError.deletionError(status)
}
print("✅ [KeychainManager] Database encryption key deleted successfully")
}
// MARK: - Private Implementation
/// Retrieves existing database encryption key from Keychain
/// - Returns: Existing 256-bit encryption key
/// - Throws: KeychainError.keyNotFound if no key exists, or other KeychainError for failures
private func getExistingDatabaseKey() async throws -> Data {
var query = keychainQuery
query[kSecReturnData as String] = true
query[kSecMatchLimit as String] = kSecMatchLimitOne
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess else {
if status == errSecItemNotFound {
throw KeychainError.keyNotFound
}
print("❌ [KeychainManager] Failed to retrieve key: \(status)")
throw KeychainError.retrievalError(status)
}
guard let keyData = result as? Data else {
print("❌ [KeychainManager] Retrieved data is not valid Data type")
throw KeychainError.invalidKeyData
}
guard keyData.count == 32 else {
print("❌ [KeychainManager] Retrieved key has invalid length: \(keyData.count) bytes (expected 32)")
throw KeychainError.invalidKeyData
}
return keyData
}
/// Generates a new 256-bit AES encryption key using iOS cryptographic APIs
/// - Returns: Cryptographically secure 256-bit key
/// - Throws: KeychainError.keyGenerationFailed if random number generation fails
private func generateEncryptionKey() throws -> Data {
var keyData = Data(count: 32) // 256-bit key
let result = keyData.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, 32, bytes.bindMemory(to: UInt8.self).baseAddress!)
}
guard result == errSecSuccess else {
print("❌ [KeychainManager] Failed to generate random key: \(result)")
throw KeychainError.keyGenerationFailed
}
print("🔑 [KeychainManager] Generated new 256-bit encryption key")
return keyData
}
/// Stores the database encryption key securely in the Keychain
/// - Parameter key: 256-bit encryption key to store
/// - Throws: KeychainError.storageError if storage fails
private func storeDatabaseKey(_ key: Data) async throws {
guard key.count == 32 else {
print("❌ [KeychainManager] Invalid key length for storage: \(key.count) bytes (expected 32)")
throw KeychainError.invalidKeyData
}
var query = keychainQuery
query[kSecValueData as String] = key
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
print("❌ [KeychainManager] Failed to store key: \(status)")
throw KeychainError.storageError(status)
}
print("✅ [KeychainManager] Database encryption key stored securely")
print("🔒 [KeychainManager] Key stored with service: \(keychainService)")
print("🔒 [KeychainManager] Key stored with account: \(databaseKeyIdentifier)")
}
// MARK: - Debugging and Diagnostics
/// Gets diagnostic information about the Keychain configuration
/// - Returns: Dictionary with diagnostic information
func getDiagnosticInfo() async -> [String: Any] {
let bundleId = Bundle.main.bundleIdentifier ?? "unknown"
let keyExists = await keyExists()
return [
"bundleId": bundleId,
"keychainService": keychainService,
"databaseKeyIdentifier": databaseKeyIdentifier,
"keyExists": keyExists,
"accessibilityLevel": "kSecAttrAccessibleWhenUnlockedThisDeviceOnly"
]
}
}
// MARK: - Extension for Configuration Integration
extension KeychainManager {
/// Validates that the KeychainManager is properly configured
/// - Throws: KeychainError if configuration is invalid
func validateConfiguration() throws {
guard Bundle.main.bundleIdentifier != nil else {
throw KeychainError.bundleIdNotAvailable
}
// Additional validation can be added here in the future
print("✅ [KeychainManager] Configuration validation passed")
}
}
This diff is collapsed. Click to expand it.
//
// TokenModel.swift
// SwiftWarplyFramework
//
// Created by Manos Chorianopoulos on 24/6/25.
//
import Foundation
/// TokenModel represents OAuth tokens with JWT parsing capabilities
/// This model handles token lifecycle management, expiration detection, and validation
struct TokenModel {
let accessToken: String
let refreshToken: String
let clientId: String?
let clientSecret: String?
let expirationDate: Date?
// MARK: - Token Lifecycle Management
/// Check if the access token is currently expired
var isExpired: Bool {
guard let expirationDate = expirationDate else {
// If we can't parse expiration, assume token is still valid
return false
}
return Date() >= expirationDate
}
/// Check if the token should be refreshed proactively (5 minutes before expiry)
var shouldRefresh: Bool {
guard let expirationDate = expirationDate else {
// If we can't parse expiration, don't refresh proactively
return false
}
// Refresh 5 minutes (300 seconds) before expiration
return Date().addingTimeInterval(300) >= expirationDate
}
/// Validate token format and structure
var isValid: Bool {
return !accessToken.isEmpty &&
!refreshToken.isEmpty &&
isValidJWTFormat(accessToken)
}
/// Get time until token expires (in seconds)
var timeUntilExpiration: TimeInterval? {
guard let expirationDate = expirationDate else { return nil }
return expirationDate.timeIntervalSinceNow
}
}
// MARK: - JWT Parsing Extension
extension TokenModel {
/// Parse JWT expiration date from access token
/// JWT structure: header.payload.signature (Base64 URL encoded)
static func parseJWTExpiration(from token: String) -> Date? {
print("🔍 [TokenModel] Parsing JWT expiration from token")
// JWT structure: header.payload.signature
let components = token.components(separatedBy: ".")
guard components.count == 3 else {
print("⚠️ [TokenModel] Invalid JWT format - expected 3 components, got \(components.count)")
return nil
}
// Decode payload (second component)
let payload = components[1]
guard let data = base64UrlDecode(payload) else {
print("⚠️ [TokenModel] Failed to decode JWT payload")
return nil
}
// Parse JSON payload
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
if let exp = json["exp"] as? TimeInterval {
let expirationDate = Date(timeIntervalSince1970: exp)
print("✅ [TokenModel] JWT expiration parsed: \(expirationDate)")
return expirationDate
} else {
print("⚠️ [TokenModel] No 'exp' claim found in JWT payload")
}
} else {
print("⚠️ [TokenModel] JWT payload is not a valid JSON object")
}
} catch {
print("❌ [TokenModel] JWT parsing error: \(error)")
}
return nil
}
/// Base64 URL decode (JWT uses URL-safe Base64 without padding)
private static func base64UrlDecode(_ string: String) -> Data? {
var base64 = string
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
// Add padding if needed (Base64 requires length to be multiple of 4)
let remainder = base64.count % 4
if remainder > 0 {
base64 += String(repeating: "=", count: 4 - remainder)
}
return Data(base64Encoded: base64)
}
}
// MARK: - Validation Methods
extension TokenModel {
/// Check if token follows JWT format (3 parts separated by dots)
private func isValidJWTFormat(_ token: String) -> Bool {
let components = token.components(separatedBy: ".")
return components.count == 3 &&
components.allSatisfy { !$0.isEmpty }
}
/// Get formatted expiration info for debugging
var expirationInfo: String {
guard let expirationDate = expirationDate else {
return "No expiration date available"
}
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .medium
if isExpired {
return "Expired at \(formatter.string(from: expirationDate))"
} else if shouldRefresh {
return "Should refresh - expires at \(formatter.string(from: expirationDate))"
} else {
return "Valid until \(formatter.string(from: expirationDate))"
}
}
/// Get time remaining until expiration in a human-readable format
var timeRemainingDescription: String {
guard let timeRemaining = timeUntilExpiration else {
return "Unknown"
}
if timeRemaining <= 0 {
return "Expired"
}
let hours = Int(timeRemaining) / 3600
let minutes = Int(timeRemaining.truncatingRemainder(dividingBy: 3600)) / 60
let seconds = Int(timeRemaining.truncatingRemainder(dividingBy: 60))
if hours > 0 {
return "\(hours)h \(minutes)m \(seconds)s"
} else if minutes > 0 {
return "\(minutes)m \(seconds)s"
} else {
return "\(seconds)s"
}
}
}
// MARK: - Convenience Initializers
extension TokenModel {
/// Initialize with automatic JWT expiration parsing
init(accessToken: String, refreshToken: String, clientId: String? = nil, clientSecret: String? = nil) {
self.accessToken = accessToken
self.refreshToken = refreshToken
self.clientId = clientId
self.clientSecret = clientSecret
self.expirationDate = Self.parseJWTExpiration(from: accessToken)
print("🔐 [TokenModel] Created token model - \(expirationInfo)")
}
/// Initialize from database values (returns nil if required tokens are missing)
init?(accessToken: String?, refreshToken: String?, clientId: String?, clientSecret: String?) {
guard let accessToken = accessToken, !accessToken.isEmpty,
let refreshToken = refreshToken, !refreshToken.isEmpty else {
print("⚠️ [TokenModel] Cannot create token model - missing required tokens")
return nil
}
self.init(
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
clientSecret: clientSecret
)
}
/// Create a new TokenModel with updated tokens (preserves client credentials)
func withUpdatedTokens(accessToken: String, refreshToken: String) -> TokenModel {
return TokenModel(
accessToken: accessToken,
refreshToken: refreshToken,
clientId: self.clientId,
clientSecret: self.clientSecret
)
}
}
// MARK: - Debug and Logging Support
extension TokenModel {
/// Safe description for logging (doesn't expose sensitive data)
var debugDescription: String {
let accessTokenPreview = String(accessToken.prefix(10)) + "..."
let refreshTokenPreview = String(refreshToken.prefix(10)) + "..."
return """
TokenModel {
accessToken: \(accessTokenPreview)
refreshToken: \(refreshTokenPreview)
clientId: \(clientId ?? "nil")
hasClientSecret: \(clientSecret != nil)
expirationDate: \(expirationDate?.description ?? "nil")
isExpired: \(isExpired)
shouldRefresh: \(shouldRefresh)
isValid: \(isValid)
timeRemaining: \(timeRemainingDescription)
}
"""
}
/// Detailed token status for debugging
var statusDescription: String {
if !isValid {
return "❌ Invalid token format"
} else if isExpired {
return "🔴 Token expired"
} else if shouldRefresh {
return "🟡 Token should be refreshed"
} else {
return "🟢 Token is valid"
}
}
}
// MARK: - Database Integration Helpers
extension TokenModel {
/// Convert to tuple format for database storage
var databaseValues: (accessToken: String, refreshToken: String, clientId: String?, clientSecret: String?) {
return (accessToken, refreshToken, clientId, clientSecret)
}
/// Create from database tuple
static func fromDatabaseValues(_ values: (accessToken: String?, refreshToken: String?, clientId: String?, clientSecret: String?)) -> TokenModel? {
return TokenModel(
accessToken: values.accessToken,
refreshToken: values.refreshToken,
clientId: values.clientId,
clientSecret: values.clientSecret
)
}
}
// MARK: - Token Refresh Support
extension TokenModel {
/// Check if this token can be used for refresh (has refresh token and client credentials)
var canRefresh: Bool {
return !refreshToken.isEmpty &&
clientId != nil &&
clientSecret != nil
}
/// Get refresh request parameters
var refreshParameters: [String: String]? {
guard let clientId = clientId,
let clientSecret = clientSecret else {
print("⚠️ [TokenModel] Cannot create refresh parameters - missing client credentials")
return nil
}
return [
"client_id": clientId,
"client_secret": clientSecret,
"refresh_token": refreshToken,
"grant_type": "refresh_token"
]
}
}
# Compilation Errors Fix Plan
## Overview
After fixing the DatabaseManager compilation errors, several new compilation errors appeared in other files. This document outlines the errors and the planned fixes.
## Current Compilation Errors
### 1. WarplySDK.swift (8 errors)
#### Error Details:
- **Line 2585**: `Value of type 'NetworkService' has no member 'setTokens'`
- **Lines 2611, 2612**: `'async' call in a function that does not support concurrency` in `constructCampaignParams(_ campaign:)`
- **Lines 2641, 2642**: `'async' call in a function that does not support concurrency` in `constructCampaignParams(campaign:isMap:)`
- **Lines 2611, 2612, 2641, 2642**: `Call can throw, but it is not marked with 'try' and the error is not handled`
#### Root Cause:
- The code is trying to call `networkService.setTokens()` which doesn't exist
- The code is calling async methods `getAccessToken()` and `getRefreshToken()` from synchronous functions
- The `constructCampaignParams` methods are synchronous but trying to call async NetworkService methods
#### Planned Fix:
- **Option A**: Make `constructCampaignParams` methods async
- **Option B**: Use DatabaseManager to get tokens synchronously (CHOSEN)
- Remove the non-existent `setTokens()` call
- Replace async NetworkService calls with synchronous DatabaseManager calls
### 2. DatabaseConfiguration.swift (3 errors)
#### Error Details:
- **Line 237**: `Cannot assign to property: 'fileProtection' is a get-only property`
- **Line 237**: `Cannot assign value of type 'FileProtectionType' to type 'URLFileProtection?'`
- **Line 239**: `Cannot use mutating member on immutable value: 'fileURL' is a 'let' constant`
#### Root Cause:
- The code is trying to set file protection using URLResourceValues incorrectly
- `fileProtection` property is read-only
- Type mismatch between `FileProtectionType` and `URLFileProtection`
- Trying to mutate an immutable URL
#### Planned Fix:
- Use FileManager.setAttributes() approach instead of URLResourceValues
- Use the correct file protection API for iOS
### 3. WarplyConfiguration.swift (1 warning)
#### Error Details:
- **Line 40**: `Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten`
#### Root Cause:
- The `frameworkVersion` property has an initial value and is immutable, so Codable can't decode it
#### Planned Fix:
- Either make the property mutable (var) or exclude it from Codable using CodingKeys
## Implementation Strategy
### Phase 1: Examine NetworkService
1. Check NetworkService.swift to understand available token management methods
2. Identify the correct method to replace `setTokens()`
### Phase 2: Fix WarplySDK Token Handling
1. Remove the non-existent `setTokens()` call
2. Replace async NetworkService calls with synchronous DatabaseManager calls
3. Update `constructCampaignParams` methods to use DatabaseManager
### Phase 3: Fix DatabaseConfiguration File Protection
1. Replace URLResourceValues approach with FileManager.setAttributes()
2. Use correct iOS file protection types
3. Handle URL mutability properly
### Phase 4: Fix WarplyConfiguration Codable
1. Add CodingKeys enum to exclude frameworkVersion from decoding
2. Keep the property immutable with initial value
## Security Considerations
### Two-Layer Security Approach:
1. **Token Encryption** (already working):
- Encrypts token data before storing in database
- Uses FieldEncryption.swift
- Protects token content
2. **File Protection** (to be fixed):
- Sets iOS file protection on database file
- Prevents file access when device is locked
- Additional security layer
## Expected Outcome
- All compilation errors resolved
- Maintain existing functionality
- Preserve both token encryption and file protection security features
- Clean, maintainable code structure
## Files Modified
1.`SwiftWarplyFramework/SwiftWarplyFramework/Core/WarplySDK.swift`
- Fixed `updateRefreshToken()` method to use DatabaseManager instead of non-existent `setTokens()`
- Fixed `constructCampaignParams()` methods to use synchronous token access from DatabaseManager
- Replaced async NetworkService calls with synchronous DatabaseManager calls
2.`SwiftWarplyFramework/SwiftWarplyFramework/Configuration/DatabaseConfiguration.swift`
- Fixed `applyFileProtection()` method to use FileManager.setAttributes() instead of URLResourceValues
- Resolved read-only property and type mismatch issues
3.`SwiftWarplyFramework/SwiftWarplyFramework/Configuration/WarplyConfiguration.swift`
- Added CodingKeys enum to exclude `frameworkVersion` from Codable encoding/decoding
- Resolved immutable property warning
## Status: COMPLETED ✅
All compilation errors have been fixed. The framework should now compile successfully with:
- Proper token management through DatabaseManager
- Working file protection for database security
- Clean Codable implementation for configuration
---
*Generated: 26/06/2025, 3:48 pm*
*Updated: 26/06/2025, 3:52 pm*
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.