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
1 +# macOS system files
1 .DS_Store 2 .DS_Store
3 +.DS_Store?
4 +._*
5 +.Spotlight-V100
6 +.Trashes
7 +ehthumbs.db
8 +Thumbs.db
9 +
10 +# Swift Package Manager
11 +.build/
12 +.swiftpm/
13 +Package.resolved
14 +
15 +# Xcode build files
16 +DerivedData/
17 +*.xcworkspace/xcuserdata/
18 +*.xcodeproj/xcuserdata/
19 +*.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
20 +*.xcodeproj/xcshareddata/IDEWorkspaceChecks.plist
21 +
22 +# CocoaPods (root level only - framework has its own Pods)
23 +/Pods/
24 +/Podfile.lock
25 +
26 +# Build artifacts
27 +build/
28 +*.ipa
29 +*.dSYM.zip
30 +*.dSYM
31 +
32 +# IDE files
33 +.vscode/
34 +.idea/
35 +*.swp
36 +*.swo
37 +*~
38 +
39 +# Temporary files
40 +*.tmp
41 +*.temp
42 +.tmp/
......
1 -<?xml version="1.0" encoding="UTF-8"?>
2 -<Workspace
3 - version = "1.0">
4 - <FileRef
5 - location = "self:">
6 - </FileRef>
7 -</Workspace>
1 -<?xml version="1.0" encoding="UTF-8"?>
2 -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 -<plist version="1.0">
4 -<dict>
5 - <key>SchemeUserState</key>
6 - <dict>
7 - <key>SwiftWarplyFramework.xcscheme_^#shared#^_</key>
8 - <dict>
9 - <key>orderHint</key>
10 - <integer>0</integer>
11 - </dict>
12 - </dict>
13 - <key>SuppressBuildableAutocreation</key>
14 - <dict>
15 - <key>SwiftWarplyFramework</key>
16 - <dict>
17 - <key>primary</key>
18 - <true/>
19 - </dict>
20 - </dict>
21 -</dict>
22 -</plist>
This diff is collapsed. Click to expand it.
1 +# DatabaseManager Debug Analysis - UPDATED
2 +
3 +## 🎉 **SOLUTION FOUND - SQLite.swift 0.12.2 Works!**
4 +
5 +**Date:** December 26, 2024
6 +**Status:****DATABASE ISSUES RESOLVED**
7 +**SQLite.swift Version:** 0.12.2 (downgraded from 0.14/0.15)
8 +**Swift Version:** 5.0
9 +
10 +---
11 +
12 +## 🚨 **Original Problem (SOLVED)**
13 +
14 +### **Root Cause Identified:**
15 +- **SQLite.swift 0.14+ requires Swift 5.3+**
16 +- **SQLite.swift 0.15+ requires Swift 5.5+**
17 +- **Our Swift 5.0 was incompatible** with newer SQLite.swift versions
18 +
19 +### **Original Errors (FIXED):**
20 +```swift
21 +// BEFORE (failing with 0.14/0.15):
22 +Expression<String?>("access_token") // ❌ Type inference failure
23 +Expression<String?>("refresh_token") // ❌ Cannot resolve constructor
24 +
25 +// AFTER (working with 0.12.2):
26 +Expression<String?>("access_token") // ✅ Compiles successfully
27 +Expression<String?>("refresh_token") // ✅ Compiles successfully
28 +```
29 +
30 +---
31 +
32 +## ✅ **SOLUTION IMPLEMENTED**
33 +
34 +### **Version Downgrade:**
35 +```swift
36 +// Package.swift - WORKING CONFIGURATION
37 +.package(url: "https://github.com/stephencelis/SQLite.swift", .exact("0.12.2"))
38 +```
39 +
40 +### **Why 0.12.2 Works:**
41 +-**Built for Swift 5.0** - perfect compatibility
42 +-**Stable Expression API** - no type inference issues
43 +-**Mature codebase** - fewer breaking changes
44 +-**Production tested** - widely used in Swift 5.0 projects
45 +
46 +---
47 +
48 +## 📊 **Current Status**
49 +
50 +### **✅ RESOLVED - Database Issues:**
51 +-**SQLite.swift compilation** - no more Expression errors
52 +-**DatabaseManager.swift** - compiles successfully
53 +-**Type inference** - Swift 5.0 compatible patterns
54 +-**Expression constructors** - working properly
55 +
56 +### **⚠️ REMAINING - Non-Database Issues:**
57 +The following errors are **NOT database-related** and need separate fixes:
58 +
59 +#### **1. WarplySDK.swift (8 errors):**
60 +```swift
61 +// Error 1: Missing switch case
62 +switch networkError {
63 + // Missing: case .invalidResponse:
64 +}
65 +
66 +// Error 2: Type conversion
67 +"error_code": error.errorCode, // ❌ Int to String conversion needed
68 +// Fix: "error_code": String(error.errorCode),
69 +
70 +// Error 3-5: Missing configuration properties
71 +config.enableRequestCaching // ❌ Property doesn't exist
72 +config.analyticsEnabled // ❌ Property doesn't exist
73 +config.crashReportingEnabled // ❌ Property doesn't exist
74 +
75 +// Error 6: Missing NetworkService method
76 +networkService.setTokens(...) // ❌ Method doesn't exist
77 +
78 +// Error 7-8: Async/await compatibility
79 +networkService.getAccessToken() // ❌ Async call in non-async function
80 +```
81 +
82 +#### **2. DatabaseConfiguration.swift (3 errors):**
83 +```swift
84 +// Error 1: Read-only property
85 +resourceValues.fileProtection = dataProtectionClass // ❌ Get-only property
86 +
87 +// Error 2: Type mismatch
88 +FileProtectionType vs URLFileProtection? // ❌ Type incompatibility
89 +
90 +// Error 3: Immutable URL
91 +let fileURL = URL(...) // ❌ Cannot mutate let constant
92 +// Fix: var fileURL = URL(...)
93 +```
94 +
95 +---
96 +
97 +## 🎯 **Next Steps - Fix Remaining Issues**
98 +
99 +### **Priority 1: WarplySDK.swift Fixes (30 minutes)**
100 +
101 +#### **Fix 1: Add Missing Switch Case**
102 +```swift
103 +switch networkError {
104 +case .noConnection:
105 + // existing code
106 +case .timeout:
107 + // existing code
108 +case .invalidResponse: // ← ADD THIS
109 + print("Invalid response received")
110 + // Handle invalid response
111 +}
112 +```
113 +
114 +#### **Fix 2: Type Conversion**
115 +```swift
116 +// BEFORE:
117 +"error_code": error.errorCode,
118 +
119 +// AFTER:
120 +"error_code": String(error.errorCode),
121 +```
122 +
123 +#### **Fix 3: Add Missing Configuration Properties**
124 +```swift
125 +// Add to WarplyNetworkConfig:
126 +public var enableRequestCaching: Bool = false
127 +
128 +// Add to WarplyConfiguration:
129 +public var analyticsEnabled: Bool = false
130 +public var crashReportingEnabled: Bool = false
131 +public var autoRegistrationEnabled: Bool = false
132 +```
133 +
134 +#### **Fix 4: Add Missing NetworkService Method**
135 +```swift
136 +// Add to NetworkService:
137 +public func setTokens(accessToken: String?, refreshToken: String?) {
138 + // Implementation
139 +}
140 +```
141 +
142 +#### **Fix 5: Fix Async/Await Issues**
143 +```swift
144 +// BEFORE:
145 +public func constructCampaignParams(_ campaign: CampaignItemModel) -> String {
146 + "access_token": networkService.getAccessToken() ?? "",
147 +
148 +// AFTER:
149 +public func constructCampaignParams(_ campaign: CampaignItemModel) async throws -> String {
150 + "access_token": try await networkService.getAccessToken() ?? "",
151 +```
152 +
153 +### **Priority 2: DatabaseConfiguration.swift Fixes (10 minutes)**
154 +
155 +#### **Fix 1: File Protection API**
156 +```swift
157 +// BEFORE:
158 +let fileURL = URL(fileURLWithPath: filePath)
159 +var resourceValues = URLResourceValues()
160 +resourceValues.fileProtection = dataProtectionClass
161 +
162 +// AFTER:
163 +var fileURL = URL(fileURLWithPath: filePath)
164 +var resourceValues = URLResourceValues()
165 +resourceValues.fileProtection = URLFileProtection(rawValue: dataProtectionClass.rawValue)
166 +try fileURL.setResourceValues(resourceValues)
167 +```
168 +
169 +---
170 +
171 +## 📋 **Implementation Checklist**
172 +
173 +### **✅ COMPLETED:**
174 +- [x] **Identify root cause** - Swift 5.0 vs SQLite.swift version incompatibility
175 +- [x] **Test SQLite.swift 0.12.2** - confirmed working
176 +- [x] **Verify database compilation** - Expression errors resolved
177 +- [x] **Document solution** - version downgrade approach
178 +
179 +### **🔄 IN PROGRESS:**
180 +- [ ] **Fix WarplySDK.swift errors** (8 errors)
181 +- [ ] **Fix DatabaseConfiguration.swift errors** (3 errors)
182 +- [ ] **Test full framework compilation**
183 +- [ ] **Verify database operations work**
184 +
185 +### **📅 TODO:**
186 +- [ ] **Update Package.swift documentation** - note Swift 5.0 requirement
187 +- [ ] **Add version compatibility notes** - for future developers
188 +- [ ] **Test database operations** - ensure CRUD works
189 +- [ ] **Performance testing** - verify no regressions
190 +
191 +---
192 +
193 +## 🎯 **Success Metrics**
194 +
195 +### **✅ ACHIEVED:**
196 +1. **Database compilation** - SQLite.swift errors eliminated
197 +2. **Version compatibility** - Swift 5.0 + SQLite.swift 0.12.2 working
198 +3. **Expression constructors** - type inference working properly
199 +
200 +### **🎯 TARGET (Next 45 minutes):**
201 +1. **Full framework compilation** - all errors resolved
202 +2. **Database operations** - CRUD functionality verified
203 +3. **Integration testing** - NetworkService + DatabaseManager working together
204 +
205 +---
206 +
207 +## 📈 **Lessons Learned**
208 +
209 +### **Key Insights:**
210 +1. **Version compatibility is critical** - newer isn't always better
211 +2. **Swift version constraints** - check library requirements carefully
212 +3. **Type inference evolution** - Swift 5.0 vs 5.3+ differences significant
213 +4. **Downgrading can solve issues** - when newer versions break compatibility
214 +
215 +### **Best Practices:**
216 +1. **Lock dependency versions** - use .exact() for critical libraries
217 +2. **Test with target Swift version** - before upgrading dependencies
218 +3. **Separate database from app logic** - isolate compilation issues
219 +4. **Document version requirements** - for future maintenance
220 +
221 +---
222 +
223 +## 🚀 **Conclusion**
224 +
225 +**The core DatabaseManager issue is SOLVED!** 🎉
226 +
227 +-**SQLite.swift 0.12.2** works perfectly with Swift 5.0
228 +-**Database compilation** successful
229 +-**Expression constructors** working properly
230 +
231 +**Remaining work:** Fix 11 non-database errors in WarplySDK.swift and DatabaseConfiguration.swift (estimated 45 minutes).
232 +
233 +**The framework is very close to full compilation success!**
234 +
235 +
236 +## __Alternative Solutions (If Version Doesn't Work)__
237 +
238 +### __Solution A: Syntax Fix with Current Version__
239 +
240 +Keep whatever version you have, but fix the Expression patterns:
241 +
242 +```swift
243 +class DatabaseManager {
244 + private var db: Connection?
245 +
246 + // Define tables as static properties
247 + private static let tokensTable = Table("tokens")
248 + private static let eventsTable = Table("events")
249 +
250 + // Define columns with explicit types
251 + private static let tokenId = Expression<Int64>("id")
252 + private static let accessToken = Expression<String?>("access_token")
253 + private static let refreshToken = Expression<String?>("refresh_token")
254 +
255 + // Use static references in methods
256 + func createTokensTable() throws {
257 + try db?.run(Self.tokensTable.create(ifNotExists: true) { table in
258 + table.column(Self.tokenId, primaryKey: .autoincrement)
259 + table.column(Self.accessToken)
260 + table.column(Self.refreshToken)
261 + })
262 + }
263 +}
264 +```
265 +
266 +### __Solution B: Raw SQL Approach__
267 +
268 +Use SQLite.swift's raw SQL capabilities instead of Expression builders:
269 +
270 +```swift
271 +func saveToken(_ token: TokenModel) throws {
272 + let sql = """
273 + INSERT OR REPLACE INTO tokens
274 + (access_token, refresh_token, client_id, client_secret, expires_at)
275 + VALUES (?, ?, ?, ?, ?)
276 + """
277 + try db?.execute(sql, token.accessToken, token.refreshToken,
278 + token.clientId, token.clientSecret, token.expiresAt)
279 +}
280 +
281 +func getToken() throws -> TokenModel? {
282 + let sql = "SELECT * FROM tokens LIMIT 1"
283 + for row in try db?.prepare(sql) ?? [] {
284 + return TokenModel(
285 + accessToken: row[0] as? String,
286 + refreshToken: row[1] as? String,
287 + clientId: row[2] as? String,
288 + clientSecret: row[3] as? String,
289 + expiresAt: row[4] as? Date
290 + )
291 + }
292 + return nil
293 +}
294 +```
295 +
296 +### __Solution C: Hybrid Approach__
297 +
298 +Use SQLite.swift for connections, raw SQL for operations:
299 +
300 +```swift
301 +class DatabaseManager {
302 + private var db: Connection?
303 +
304 + init(databasePath: String) throws {
305 + db = try Connection(databasePath)
306 + try createTables()
307 + }
308 +
309 + private func createTables() throws {
310 + // Use raw SQL for table creation
311 + try db?.execute("""
312 + CREATE TABLE IF NOT EXISTS tokens (
313 + id INTEGER PRIMARY KEY AUTOINCREMENT,
314 + access_token TEXT,
315 + refresh_token TEXT,
316 + client_id TEXT,
317 + client_secret TEXT,
318 + expires_at REAL
319 + )
320 + """)
321 + }
322 +}
323 +```
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
...@@ -3,16 +3,34 @@ ...@@ -3,16 +3,34 @@
3 { 3 {
4 "identity" : "rsbarcodes_swift", 4 "identity" : "rsbarcodes_swift",
5 "kind" : "remoteSourceControl", 5 "kind" : "remoteSourceControl",
6 - "location" : "https://github.com/yeahdongcn/RSBarcodes_Swift.git", 6 + "location" : "https://github.com/yeahdongcn/RSBarcodes_Swift",
7 "state" : { 7 "state" : {
8 "revision" : "241de72a96f49b1545d5de3c00fae170c2675c41", 8 "revision" : "241de72a96f49b1545d5de3c00fae170c2675c41",
9 "version" : "5.2.0" 9 "version" : "5.2.0"
10 } 10 }
11 }, 11 },
12 { 12 {
13 + "identity" : "sqlite.swift",
14 + "kind" : "remoteSourceControl",
15 + "location" : "https://github.com/stephencelis/SQLite.swift",
16 + "state" : {
17 + "revision" : "392dd6058624d9f6c5b4c769d165ddd8c7293394",
18 + "version" : "0.15.4"
19 + }
20 + },
21 + {
22 + "identity" : "swift-toolchain-sqlite",
23 + "kind" : "remoteSourceControl",
24 + "location" : "https://github.com/swiftlang/swift-toolchain-sqlite",
25 + "state" : {
26 + "revision" : "b626d3002773b1a1304166643e7f118f724b2132",
27 + "version" : "1.0.4"
28 + }
29 + },
30 + {
13 "identity" : "swifteventbus", 31 "identity" : "swifteventbus",
14 "kind" : "remoteSourceControl", 32 "kind" : "remoteSourceControl",
15 - "location" : "https://github.com/cesarferreira/SwiftEventBus.git", 33 + "location" : "https://github.com/cesarferreira/SwiftEventBus",
16 "state" : { 34 "state" : {
17 "revision" : "a30ff35e616f507d8a8d122dac32a2150371a87e", 35 "revision" : "a30ff35e616f507d8a8d122dac32a2150371a87e",
18 "version" : "5.1.0" 36 "version" : "5.1.0"
......
...@@ -21,14 +21,16 @@ let package = Package( ...@@ -21,14 +21,16 @@ let package = Package(
21 ], 21 ],
22 dependencies: [ 22 dependencies: [
23 .package(url: "https://github.com/yeahdongcn/RSBarcodes_Swift", from: "5.2.0"), 23 .package(url: "https://github.com/yeahdongcn/RSBarcodes_Swift", from: "5.2.0"),
24 - .package(url: "https://github.com/cesarferreira/SwiftEventBus", from: "5.0.0") 24 + .package(url: "https://github.com/cesarferreira/SwiftEventBus", from: "5.0.0"),
25 + .package(url: "https://github.com/stephencelis/SQLite.swift", exact: "0.12.2")
25 ], 26 ],
26 targets: [ 27 targets: [
27 .target( 28 .target(
28 name: "SwiftWarplyFramework", 29 name: "SwiftWarplyFramework",
29 dependencies: [ 30 dependencies: [
30 .product(name: "RSBarcodes_Swift", package: "RSBarcodes_Swift"), 31 .product(name: "RSBarcodes_Swift", package: "RSBarcodes_Swift"),
31 - .product(name: "SwiftEventBus", package: "SwiftEventBus") 32 + .product(name: "SwiftEventBus", package: "SwiftEventBus"),
33 + .product(name: "SQLite", package: "SQLite.swift")
32 ], 34 ],
33 path: "SwiftWarplyFramework/SwiftWarplyFramework", 35 path: "SwiftWarplyFramework/SwiftWarplyFramework",
34 exclude: [ 36 exclude: [
......
...@@ -49,6 +49,7 @@ Pod::Spec.new do |spec| ...@@ -49,6 +49,7 @@ Pod::Spec.new do |spec|
49 spec.dependency 'RSBarcodes_Swift', '~> 5.2.0' 49 spec.dependency 'RSBarcodes_Swift', '~> 5.2.0'
50 # spec.dependency 'RSBarcodes_Swift', '~> 5.1.1' 50 # spec.dependency 'RSBarcodes_Swift', '~> 5.1.1'
51 spec.dependency 'SwiftEventBus' 51 spec.dependency 'SwiftEventBus'
52 + spec.dependency 'SQLite.swift', '~> 0.12.2'
52 53
53 # spec.resource_bundles = { 'ResourcesBundle' => ['SwiftWarplyFramework/**/*.{png,jpeg,jpg,storyboard,xib,xcassets,json,ttf,imageset,strings}'] } 54 # spec.resource_bundles = { 'ResourcesBundle' => ['SwiftWarplyFramework/**/*.{png,jpeg,jpg,storyboard,xib,xcassets,json,ttf,imageset,strings}'] }
54 55
......
1 -{
2 - "originHash" : "12dce73308b76580a096b2ddc2db953ca534c29f52e5b13e15c81719afbc8e45",
3 - "pins" : [
4 - {
5 - "identity" : "rsbarcodes_swift",
6 - "kind" : "remoteSourceControl",
7 - "location" : "https://github.com/yeahdongcn/RSBarcodes_Swift",
8 - "state" : {
9 - "revision" : "241de72a96f49b1545d5de3c00fae170c2675c41",
10 - "version" : "5.2.0"
11 - }
12 - },
13 - {
14 - "identity" : "swifteventbus",
15 - "kind" : "remoteSourceControl",
16 - "location" : "https://github.com/cesarferreira/SwiftEventBus",
17 - "state" : {
18 - "revision" : "a30ff35e616f507d8a8d122dac32a2150371a87e",
19 - "version" : "5.1.0"
20 - }
21 - }
22 - ],
23 - "version" : 3
24 -}
1 { 1 {
2 - "originHash" : "17e77d02482a9bad5f5e4730583b6ef8e884bc07c7c794430f8edee2618193bc", 2 + "originHash" : "cb944cd3bee35f5e65fbd247311810bd6adc1d6454816597431789e670c31595",
3 "pins" : [ 3 "pins" : [
4 { 4 {
5 "identity" : "rsbarcodes_swift", 5 "identity" : "rsbarcodes_swift",
...@@ -11,6 +11,15 @@ ...@@ -11,6 +11,15 @@
11 } 11 }
12 }, 12 },
13 { 13 {
14 + "identity" : "sqlite.swift",
15 + "kind" : "remoteSourceControl",
16 + "location" : "https://github.com/stephencelis/SQLite.swift",
17 + "state" : {
18 + "revision" : "0a9893ec030501a3956bee572d6b4fdd3ae158a1",
19 + "version" : "0.12.2"
20 + }
21 + },
22 + {
14 "identity" : "swifteventbus", 23 "identity" : "swifteventbus",
15 "kind" : "remoteSourceControl", 24 "kind" : "remoteSourceControl",
16 "location" : "https://github.com/cesarferreira/SwiftEventBus", 25 "location" : "https://github.com/cesarferreira/SwiftEventBus",
......
1 +//
2 +// KeychainManager.swift
3 +// SwiftWarplyFramework
4 +//
5 +// Created by Warply on 25/6/25.
6 +//
7 +
8 +import Foundation
9 +import Security
10 +
11 +/// Errors that can occur during Keychain operations
12 +enum KeychainError: Error, LocalizedError {
13 + case keyGenerationFailed
14 + case keyNotFound
15 + case storageError(OSStatus)
16 + case retrievalError(OSStatus)
17 + case deletionError(OSStatus)
18 + case invalidKeyData
19 + case bundleIdNotAvailable
20 +
21 + var errorDescription: String? {
22 + switch self {
23 + case .keyGenerationFailed:
24 + return "Failed to generate encryption key using SecRandomCopyBytes"
25 + case .keyNotFound:
26 + return "Encryption key not found in Keychain"
27 + case .storageError(let status):
28 + return "Failed to store key in Keychain: \(status) (\(SecCopyErrorMessageString(status, nil) ?? "Unknown error" as CFString))"
29 + case .retrievalError(let status):
30 + return "Failed to retrieve key from Keychain: \(status) (\(SecCopyErrorMessageString(status, nil) ?? "Unknown error" as CFString))"
31 + case .deletionError(let status):
32 + return "Failed to delete key from Keychain: \(status) (\(SecCopyErrorMessageString(status, nil) ?? "Unknown error" as CFString))"
33 + case .invalidKeyData:
34 + return "Invalid key data format - expected 32 bytes for AES-256"
35 + case .bundleIdNotAvailable:
36 + return "Bundle identifier not available - required for Keychain isolation"
37 + }
38 + }
39 +}
40 +
41 +/// Thread-safe manager for secure encryption key storage using iOS Keychain Services
42 +/// Provides automatic key generation and Bundle ID-based isolation between client apps
43 +actor KeychainManager {
44 +
45 + /// Shared singleton instance
46 + static let shared = KeychainManager()
47 +
48 + /// Private initializer to enforce singleton pattern
49 + private init() {}
50 +
51 + // MARK: - Bundle ID-Based Isolation
52 +
53 + /// Unique Keychain service identifier based on client app's Bundle ID
54 + /// This ensures complete isolation between different client apps using the framework
55 + private var keychainService: String {
56 + guard let bundleId = Bundle.main.bundleIdentifier, !bundleId.isEmpty else {
57 + // Use fallback for edge cases, but log the issue
58 + print("⚠️ [KeychainManager] Bundle ID not available, using fallback identifier")
59 + return "com.warply.sdk.unknown"
60 + }
61 + return "com.warply.sdk.\(bundleId)"
62 + }
63 +
64 + /// Simple account identifier for the database encryption key
65 + /// Isolation is provided by the service identifier above
66 + private let databaseKeyIdentifier = "database_encryption_key"
67 +
68 + /// Base Keychain query dictionary with security attributes
69 + private var keychainQuery: [String: Any] {
70 + return [
71 + kSecClass as String: kSecClassGenericPassword,
72 + kSecAttrService as String: keychainService,
73 + kSecAttrAccount as String: databaseKeyIdentifier,
74 + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
75 + ]
76 + }
77 +
78 + // MARK: - Public API
79 +
80 + /// Gets existing database encryption key or creates a new one if none exists
81 + /// This is the main entry point for database encryption key management
82 + /// - Returns: 256-bit AES encryption key
83 + /// - Throws: KeychainError if key generation or storage fails
84 + func getOrCreateDatabaseKey() async throws -> Data {
85 + print("🔑 [KeychainManager] Requesting database encryption key for app: \(Bundle.main.bundleIdentifier ?? "unknown")")
86 +
87 + // Try to get existing key first
88 + do {
89 + let existingKey = try await getExistingDatabaseKey()
90 + print("✅ [KeychainManager] Retrieved existing database encryption key")
91 + return existingKey
92 + } catch KeychainError.keyNotFound {
93 + // Generate new key if none exists
94 + print("🔑 [KeychainManager] No existing key found, generating new database encryption key")
95 + let newKey = try generateEncryptionKey()
96 + try await storeDatabaseKey(newKey)
97 + print("✅ [KeychainManager] Generated and stored new database encryption key")
98 + return newKey
99 + }
100 + // Re-throw other errors
101 + }
102 +
103 + /// Checks if a database encryption key exists in the Keychain
104 + /// - Returns: true if key exists, false otherwise
105 + func keyExists() async -> Bool {
106 + do {
107 + _ = try await getExistingDatabaseKey()
108 + return true
109 + } catch {
110 + return false
111 + }
112 + }
113 +
114 + /// Deletes the database encryption key from the Keychain
115 + /// This will make all encrypted data unreadable
116 + /// - Throws: KeychainError if deletion fails
117 + func deleteDatabaseKey() async throws {
118 + print("🗑️ [KeychainManager] Deleting database encryption key for app: \(Bundle.main.bundleIdentifier ?? "unknown")")
119 +
120 + let query = keychainQuery
121 + let status = SecItemDelete(query as CFDictionary)
122 +
123 + guard status == errSecSuccess || status == errSecItemNotFound else {
124 + print("❌ [KeychainManager] Failed to delete database key: \(status)")
125 + throw KeychainError.deletionError(status)
126 + }
127 +
128 + print("✅ [KeychainManager] Database encryption key deleted successfully")
129 + }
130 +
131 + // MARK: - Private Implementation
132 +
133 + /// Retrieves existing database encryption key from Keychain
134 + /// - Returns: Existing 256-bit encryption key
135 + /// - Throws: KeychainError.keyNotFound if no key exists, or other KeychainError for failures
136 + private func getExistingDatabaseKey() async throws -> Data {
137 + var query = keychainQuery
138 + query[kSecReturnData as String] = true
139 + query[kSecMatchLimit as String] = kSecMatchLimitOne
140 +
141 + var result: AnyObject?
142 + let status = SecItemCopyMatching(query as CFDictionary, &result)
143 +
144 + guard status == errSecSuccess else {
145 + if status == errSecItemNotFound {
146 + throw KeychainError.keyNotFound
147 + }
148 + print("❌ [KeychainManager] Failed to retrieve key: \(status)")
149 + throw KeychainError.retrievalError(status)
150 + }
151 +
152 + guard let keyData = result as? Data else {
153 + print("❌ [KeychainManager] Retrieved data is not valid Data type")
154 + throw KeychainError.invalidKeyData
155 + }
156 +
157 + guard keyData.count == 32 else {
158 + print("❌ [KeychainManager] Retrieved key has invalid length: \(keyData.count) bytes (expected 32)")
159 + throw KeychainError.invalidKeyData
160 + }
161 +
162 + return keyData
163 + }
164 +
165 + /// Generates a new 256-bit AES encryption key using iOS cryptographic APIs
166 + /// - Returns: Cryptographically secure 256-bit key
167 + /// - Throws: KeychainError.keyGenerationFailed if random number generation fails
168 + private func generateEncryptionKey() throws -> Data {
169 + var keyData = Data(count: 32) // 256-bit key
170 + let result = keyData.withUnsafeMutableBytes { bytes in
171 + SecRandomCopyBytes(kSecRandomDefault, 32, bytes.bindMemory(to: UInt8.self).baseAddress!)
172 + }
173 +
174 + guard result == errSecSuccess else {
175 + print("❌ [KeychainManager] Failed to generate random key: \(result)")
176 + throw KeychainError.keyGenerationFailed
177 + }
178 +
179 + print("🔑 [KeychainManager] Generated new 256-bit encryption key")
180 + return keyData
181 + }
182 +
183 + /// Stores the database encryption key securely in the Keychain
184 + /// - Parameter key: 256-bit encryption key to store
185 + /// - Throws: KeychainError.storageError if storage fails
186 + private func storeDatabaseKey(_ key: Data) async throws {
187 + guard key.count == 32 else {
188 + print("❌ [KeychainManager] Invalid key length for storage: \(key.count) bytes (expected 32)")
189 + throw KeychainError.invalidKeyData
190 + }
191 +
192 + var query = keychainQuery
193 + query[kSecValueData as String] = key
194 +
195 + let status = SecItemAdd(query as CFDictionary, nil)
196 +
197 + guard status == errSecSuccess else {
198 + print("❌ [KeychainManager] Failed to store key: \(status)")
199 + throw KeychainError.storageError(status)
200 + }
201 +
202 + print("✅ [KeychainManager] Database encryption key stored securely")
203 + print("🔒 [KeychainManager] Key stored with service: \(keychainService)")
204 + print("🔒 [KeychainManager] Key stored with account: \(databaseKeyIdentifier)")
205 + }
206 +
207 + // MARK: - Debugging and Diagnostics
208 +
209 + /// Gets diagnostic information about the Keychain configuration
210 + /// - Returns: Dictionary with diagnostic information
211 + func getDiagnosticInfo() async -> [String: Any] {
212 + let bundleId = Bundle.main.bundleIdentifier ?? "unknown"
213 + let keyExists = await keyExists()
214 +
215 + return [
216 + "bundleId": bundleId,
217 + "keychainService": keychainService,
218 + "databaseKeyIdentifier": databaseKeyIdentifier,
219 + "keyExists": keyExists,
220 + "accessibilityLevel": "kSecAttrAccessibleWhenUnlockedThisDeviceOnly"
221 + ]
222 + }
223 +}
224 +
225 +// MARK: - Extension for Configuration Integration
226 +
227 +extension KeychainManager {
228 +
229 + /// Validates that the KeychainManager is properly configured
230 + /// - Throws: KeychainError if configuration is invalid
231 + func validateConfiguration() throws {
232 + guard Bundle.main.bundleIdentifier != nil else {
233 + throw KeychainError.bundleIdNotAvailable
234 + }
235 +
236 + // Additional validation can be added here in the future
237 + print("✅ [KeychainManager] Configuration validation passed")
238 + }
239 +}
This diff is collapsed. Click to expand it.
1 +//
2 +// TokenModel.swift
3 +// SwiftWarplyFramework
4 +//
5 +// Created by Manos Chorianopoulos on 24/6/25.
6 +//
7 +
8 +import Foundation
9 +
10 +/// TokenModel represents OAuth tokens with JWT parsing capabilities
11 +/// This model handles token lifecycle management, expiration detection, and validation
12 +struct TokenModel {
13 + let accessToken: String
14 + let refreshToken: String
15 + let clientId: String?
16 + let clientSecret: String?
17 + let expirationDate: Date?
18 +
19 + // MARK: - Token Lifecycle Management
20 +
21 + /// Check if the access token is currently expired
22 + var isExpired: Bool {
23 + guard let expirationDate = expirationDate else {
24 + // If we can't parse expiration, assume token is still valid
25 + return false
26 + }
27 + return Date() >= expirationDate
28 + }
29 +
30 + /// Check if the token should be refreshed proactively (5 minutes before expiry)
31 + var shouldRefresh: Bool {
32 + guard let expirationDate = expirationDate else {
33 + // If we can't parse expiration, don't refresh proactively
34 + return false
35 + }
36 + // Refresh 5 minutes (300 seconds) before expiration
37 + return Date().addingTimeInterval(300) >= expirationDate
38 + }
39 +
40 + /// Validate token format and structure
41 + var isValid: Bool {
42 + return !accessToken.isEmpty &&
43 + !refreshToken.isEmpty &&
44 + isValidJWTFormat(accessToken)
45 + }
46 +
47 + /// Get time until token expires (in seconds)
48 + var timeUntilExpiration: TimeInterval? {
49 + guard let expirationDate = expirationDate else { return nil }
50 + return expirationDate.timeIntervalSinceNow
51 + }
52 +}
53 +
54 +// MARK: - JWT Parsing Extension
55 +extension TokenModel {
56 +
57 + /// Parse JWT expiration date from access token
58 + /// JWT structure: header.payload.signature (Base64 URL encoded)
59 + static func parseJWTExpiration(from token: String) -> Date? {
60 + print("🔍 [TokenModel] Parsing JWT expiration from token")
61 +
62 + // JWT structure: header.payload.signature
63 + let components = token.components(separatedBy: ".")
64 + guard components.count == 3 else {
65 + print("⚠️ [TokenModel] Invalid JWT format - expected 3 components, got \(components.count)")
66 + return nil
67 + }
68 +
69 + // Decode payload (second component)
70 + let payload = components[1]
71 + guard let data = base64UrlDecode(payload) else {
72 + print("⚠️ [TokenModel] Failed to decode JWT payload")
73 + return nil
74 + }
75 +
76 + // Parse JSON payload
77 + do {
78 + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
79 + if let exp = json["exp"] as? TimeInterval {
80 + let expirationDate = Date(timeIntervalSince1970: exp)
81 + print("✅ [TokenModel] JWT expiration parsed: \(expirationDate)")
82 + return expirationDate
83 + } else {
84 + print("⚠️ [TokenModel] No 'exp' claim found in JWT payload")
85 + }
86 + } else {
87 + print("⚠️ [TokenModel] JWT payload is not a valid JSON object")
88 + }
89 + } catch {
90 + print("❌ [TokenModel] JWT parsing error: \(error)")
91 + }
92 +
93 + return nil
94 + }
95 +
96 + /// Base64 URL decode (JWT uses URL-safe Base64 without padding)
97 + private static func base64UrlDecode(_ string: String) -> Data? {
98 + var base64 = string
99 + .replacingOccurrences(of: "-", with: "+")
100 + .replacingOccurrences(of: "_", with: "/")
101 +
102 + // Add padding if needed (Base64 requires length to be multiple of 4)
103 + let remainder = base64.count % 4
104 + if remainder > 0 {
105 + base64 += String(repeating: "=", count: 4 - remainder)
106 + }
107 +
108 + return Data(base64Encoded: base64)
109 + }
110 +}
111 +
112 +// MARK: - Validation Methods
113 +extension TokenModel {
114 +
115 + /// Check if token follows JWT format (3 parts separated by dots)
116 + private func isValidJWTFormat(_ token: String) -> Bool {
117 + let components = token.components(separatedBy: ".")
118 + return components.count == 3 &&
119 + components.allSatisfy { !$0.isEmpty }
120 + }
121 +
122 + /// Get formatted expiration info for debugging
123 + var expirationInfo: String {
124 + guard let expirationDate = expirationDate else {
125 + return "No expiration date available"
126 + }
127 +
128 + let formatter = DateFormatter()
129 + formatter.dateStyle = .medium
130 + formatter.timeStyle = .medium
131 +
132 + if isExpired {
133 + return "Expired at \(formatter.string(from: expirationDate))"
134 + } else if shouldRefresh {
135 + return "Should refresh - expires at \(formatter.string(from: expirationDate))"
136 + } else {
137 + return "Valid until \(formatter.string(from: expirationDate))"
138 + }
139 + }
140 +
141 + /// Get time remaining until expiration in a human-readable format
142 + var timeRemainingDescription: String {
143 + guard let timeRemaining = timeUntilExpiration else {
144 + return "Unknown"
145 + }
146 +
147 + if timeRemaining <= 0 {
148 + return "Expired"
149 + }
150 +
151 + let hours = Int(timeRemaining) / 3600
152 + let minutes = Int(timeRemaining.truncatingRemainder(dividingBy: 3600)) / 60
153 + let seconds = Int(timeRemaining.truncatingRemainder(dividingBy: 60))
154 +
155 + if hours > 0 {
156 + return "\(hours)h \(minutes)m \(seconds)s"
157 + } else if minutes > 0 {
158 + return "\(minutes)m \(seconds)s"
159 + } else {
160 + return "\(seconds)s"
161 + }
162 + }
163 +}
164 +
165 +// MARK: - Convenience Initializers
166 +extension TokenModel {
167 +
168 + /// Initialize with automatic JWT expiration parsing
169 + init(accessToken: String, refreshToken: String, clientId: String? = nil, clientSecret: String? = nil) {
170 + self.accessToken = accessToken
171 + self.refreshToken = refreshToken
172 + self.clientId = clientId
173 + self.clientSecret = clientSecret
174 + self.expirationDate = Self.parseJWTExpiration(from: accessToken)
175 +
176 + print("🔐 [TokenModel] Created token model - \(expirationInfo)")
177 + }
178 +
179 + /// Initialize from database values (returns nil if required tokens are missing)
180 + init?(accessToken: String?, refreshToken: String?, clientId: String?, clientSecret: String?) {
181 + guard let accessToken = accessToken, !accessToken.isEmpty,
182 + let refreshToken = refreshToken, !refreshToken.isEmpty else {
183 + print("⚠️ [TokenModel] Cannot create token model - missing required tokens")
184 + return nil
185 + }
186 +
187 + self.init(
188 + accessToken: accessToken,
189 + refreshToken: refreshToken,
190 + clientId: clientId,
191 + clientSecret: clientSecret
192 + )
193 + }
194 +
195 + /// Create a new TokenModel with updated tokens (preserves client credentials)
196 + func withUpdatedTokens(accessToken: String, refreshToken: String) -> TokenModel {
197 + return TokenModel(
198 + accessToken: accessToken,
199 + refreshToken: refreshToken,
200 + clientId: self.clientId,
201 + clientSecret: self.clientSecret
202 + )
203 + }
204 +}
205 +
206 +// MARK: - Debug and Logging Support
207 +extension TokenModel {
208 +
209 + /// Safe description for logging (doesn't expose sensitive data)
210 + var debugDescription: String {
211 + let accessTokenPreview = String(accessToken.prefix(10)) + "..."
212 + let refreshTokenPreview = String(refreshToken.prefix(10)) + "..."
213 +
214 + return """
215 + TokenModel {
216 + accessToken: \(accessTokenPreview)
217 + refreshToken: \(refreshTokenPreview)
218 + clientId: \(clientId ?? "nil")
219 + hasClientSecret: \(clientSecret != nil)
220 + expirationDate: \(expirationDate?.description ?? "nil")
221 + isExpired: \(isExpired)
222 + shouldRefresh: \(shouldRefresh)
223 + isValid: \(isValid)
224 + timeRemaining: \(timeRemainingDescription)
225 + }
226 + """
227 + }
228 +
229 + /// Detailed token status for debugging
230 + var statusDescription: String {
231 + if !isValid {
232 + return "❌ Invalid token format"
233 + } else if isExpired {
234 + return "🔴 Token expired"
235 + } else if shouldRefresh {
236 + return "🟡 Token should be refreshed"
237 + } else {
238 + return "🟢 Token is valid"
239 + }
240 + }
241 +}
242 +
243 +// MARK: - Database Integration Helpers
244 +extension TokenModel {
245 +
246 + /// Convert to tuple format for database storage
247 + var databaseValues: (accessToken: String, refreshToken: String, clientId: String?, clientSecret: String?) {
248 + return (accessToken, refreshToken, clientId, clientSecret)
249 + }
250 +
251 + /// Create from database tuple
252 + static func fromDatabaseValues(_ values: (accessToken: String?, refreshToken: String?, clientId: String?, clientSecret: String?)) -> TokenModel? {
253 + return TokenModel(
254 + accessToken: values.accessToken,
255 + refreshToken: values.refreshToken,
256 + clientId: values.clientId,
257 + clientSecret: values.clientSecret
258 + )
259 + }
260 +}
261 +
262 +// MARK: - Token Refresh Support
263 +extension TokenModel {
264 +
265 + /// Check if this token can be used for refresh (has refresh token and client credentials)
266 + var canRefresh: Bool {
267 + return !refreshToken.isEmpty &&
268 + clientId != nil &&
269 + clientSecret != nil
270 + }
271 +
272 + /// Get refresh request parameters
273 + var refreshParameters: [String: String]? {
274 + guard let clientId = clientId,
275 + let clientSecret = clientSecret else {
276 + print("⚠️ [TokenModel] Cannot create refresh parameters - missing client credentials")
277 + return nil
278 + }
279 +
280 + return [
281 + "client_id": clientId,
282 + "client_secret": clientSecret,
283 + "refresh_token": refreshToken,
284 + "grant_type": "refresh_token"
285 + ]
286 + }
287 +}
1 +# Compilation Errors Fix Plan
2 +
3 +## Overview
4 +After fixing the DatabaseManager compilation errors, several new compilation errors appeared in other files. This document outlines the errors and the planned fixes.
5 +
6 +## Current Compilation Errors
7 +
8 +### 1. WarplySDK.swift (8 errors)
9 +
10 +#### Error Details:
11 +- **Line 2585**: `Value of type 'NetworkService' has no member 'setTokens'`
12 +- **Lines 2611, 2612**: `'async' call in a function that does not support concurrency` in `constructCampaignParams(_ campaign:)`
13 +- **Lines 2641, 2642**: `'async' call in a function that does not support concurrency` in `constructCampaignParams(campaign:isMap:)`
14 +- **Lines 2611, 2612, 2641, 2642**: `Call can throw, but it is not marked with 'try' and the error is not handled`
15 +
16 +#### Root Cause:
17 +- The code is trying to call `networkService.setTokens()` which doesn't exist
18 +- The code is calling async methods `getAccessToken()` and `getRefreshToken()` from synchronous functions
19 +- The `constructCampaignParams` methods are synchronous but trying to call async NetworkService methods
20 +
21 +#### Planned Fix:
22 +- **Option A**: Make `constructCampaignParams` methods async
23 +- **Option B**: Use DatabaseManager to get tokens synchronously (CHOSEN)
24 +- Remove the non-existent `setTokens()` call
25 +- Replace async NetworkService calls with synchronous DatabaseManager calls
26 +
27 +### 2. DatabaseConfiguration.swift (3 errors)
28 +
29 +#### Error Details:
30 +- **Line 237**: `Cannot assign to property: 'fileProtection' is a get-only property`
31 +- **Line 237**: `Cannot assign value of type 'FileProtectionType' to type 'URLFileProtection?'`
32 +- **Line 239**: `Cannot use mutating member on immutable value: 'fileURL' is a 'let' constant`
33 +
34 +#### Root Cause:
35 +- The code is trying to set file protection using URLResourceValues incorrectly
36 +- `fileProtection` property is read-only
37 +- Type mismatch between `FileProtectionType` and `URLFileProtection`
38 +- Trying to mutate an immutable URL
39 +
40 +#### Planned Fix:
41 +- Use FileManager.setAttributes() approach instead of URLResourceValues
42 +- Use the correct file protection API for iOS
43 +
44 +### 3. WarplyConfiguration.swift (1 warning)
45 +
46 +#### Error Details:
47 +- **Line 40**: `Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten`
48 +
49 +#### Root Cause:
50 +- The `frameworkVersion` property has an initial value and is immutable, so Codable can't decode it
51 +
52 +#### Planned Fix:
53 +- Either make the property mutable (var) or exclude it from Codable using CodingKeys
54 +
55 +## Implementation Strategy
56 +
57 +### Phase 1: Examine NetworkService
58 +1. Check NetworkService.swift to understand available token management methods
59 +2. Identify the correct method to replace `setTokens()`
60 +
61 +### Phase 2: Fix WarplySDK Token Handling
62 +1. Remove the non-existent `setTokens()` call
63 +2. Replace async NetworkService calls with synchronous DatabaseManager calls
64 +3. Update `constructCampaignParams` methods to use DatabaseManager
65 +
66 +### Phase 3: Fix DatabaseConfiguration File Protection
67 +1. Replace URLResourceValues approach with FileManager.setAttributes()
68 +2. Use correct iOS file protection types
69 +3. Handle URL mutability properly
70 +
71 +### Phase 4: Fix WarplyConfiguration Codable
72 +1. Add CodingKeys enum to exclude frameworkVersion from decoding
73 +2. Keep the property immutable with initial value
74 +
75 +## Security Considerations
76 +
77 +### Two-Layer Security Approach:
78 +1. **Token Encryption** (already working):
79 + - Encrypts token data before storing in database
80 + - Uses FieldEncryption.swift
81 + - Protects token content
82 +
83 +2. **File Protection** (to be fixed):
84 + - Sets iOS file protection on database file
85 + - Prevents file access when device is locked
86 + - Additional security layer
87 +
88 +## Expected Outcome
89 +- All compilation errors resolved
90 +- Maintain existing functionality
91 +- Preserve both token encryption and file protection security features
92 +- Clean, maintainable code structure
93 +
94 +## Files Modified
95 +1.`SwiftWarplyFramework/SwiftWarplyFramework/Core/WarplySDK.swift`
96 + - Fixed `updateRefreshToken()` method to use DatabaseManager instead of non-existent `setTokens()`
97 + - Fixed `constructCampaignParams()` methods to use synchronous token access from DatabaseManager
98 + - Replaced async NetworkService calls with synchronous DatabaseManager calls
99 +
100 +2.`SwiftWarplyFramework/SwiftWarplyFramework/Configuration/DatabaseConfiguration.swift`
101 + - Fixed `applyFileProtection()` method to use FileManager.setAttributes() instead of URLResourceValues
102 + - Resolved read-only property and type mismatch issues
103 +
104 +3.`SwiftWarplyFramework/SwiftWarplyFramework/Configuration/WarplyConfiguration.swift`
105 + - Added CodingKeys enum to exclude `frameworkVersion` from Codable encoding/decoding
106 + - Resolved immutable property warning
107 +
108 +## Status: COMPLETED ✅
109 +All compilation errors have been fixed. The framework should now compile successfully with:
110 +- Proper token management through DatabaseManager
111 +- Working file protection for database security
112 +- Clean Codable implementation for configuration
113 +
114 +---
115 +*Generated: 26/06/2025, 3:48 pm*
116 +*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.