Files
daily-notification-plugin/ios/Plugin/DailyNotificationDatabase.swift
Server 5844b92e18 feat(ios): implement Phase 1 permission methods and fix build issues
Implement checkPermissionStatus() and requestNotificationPermissions()
methods for iOS plugin, matching Android functionality. Fix compilation
errors across plugin files and add comprehensive build/test infrastructure.

Key Changes:
- Add checkPermissionStatus() and requestNotificationPermissions() methods
- Fix 13+ categories of Swift compilation errors (type conversions, logger
  API, access control, async/await, etc.)
- Create DailyNotificationScheduler, DailyNotificationStorage,
  DailyNotificationStateActor, and DailyNotificationErrorCodes components
- Fix CoreData initialization to handle missing model gracefully for Phase 1
- Add iOS test app build script with simulator auto-detection
- Update directive with lessons learned from build and permission work

Build Status:  BUILD SUCCEEDED
Test App:  Ready for iOS Simulator testing

Files Modified:
- doc/directives/0003-iOS-Android-Parity-Directive.md (lessons learned)
- ios/Plugin/DailyNotificationPlugin.swift (Phase 1 methods)
- ios/Plugin/DailyNotificationModel.swift (CoreData fix)
- 11+ other plugin files (compilation fixes)

Files Added:
- ios/Plugin/DailyNotificationScheduler.swift
- ios/Plugin/DailyNotificationStorage.swift
- ios/Plugin/DailyNotificationStateActor.swift
- ios/Plugin/DailyNotificationErrorCodes.swift
- scripts/build-ios-test-app.sh
- scripts/setup-ios-test-app.sh
- test-apps/ios-test-app/ (full test app)
- Multiple Phase 1 documentation files
2025-11-13 05:14:24 -08:00

241 lines
7.2 KiB
Swift

/**
* DailyNotificationDatabase.swift
*
* iOS SQLite database management for daily notifications
*
* @author Matthew Raymer
* @version 1.0.0
*/
import Foundation
import SQLite3
/**
* SQLite database manager for daily notifications on iOS
*
* This class manages the SQLite database with the three-table schema:
* - notif_contents: keep history, fast newest-first reads
* - notif_deliveries: track many deliveries per slot/time
* - notif_config: generic configuration KV
*/
class DailyNotificationDatabase {
// MARK: - Constants
private static let TAG = "DailyNotificationDatabase"
// Table names
static let TABLE_NOTIF_CONTENTS = "notif_contents"
static let TABLE_NOTIF_DELIVERIES = "notif_deliveries"
static let TABLE_NOTIF_CONFIG = "notif_config"
// Column names
static let COL_CONTENTS_ID = "id"
static let COL_CONTENTS_SLOT_ID = "slot_id"
static let COL_CONTENTS_PAYLOAD_JSON = "payload_json"
static let COL_CONTENTS_FETCHED_AT = "fetched_at"
static let COL_CONTENTS_ETAG = "etag"
static let COL_DELIVERIES_ID = "id"
static let COL_DELIVERIES_SLOT_ID = "slot_id"
static let COL_DELIVERIES_FIRE_AT = "fire_at"
static let COL_DELIVERIES_DELIVERED_AT = "delivered_at"
static let COL_DELIVERIES_STATUS = "status"
static let COL_DELIVERIES_ERROR_CODE = "error_code"
static let COL_DELIVERIES_ERROR_MESSAGE = "error_message"
static let COL_CONFIG_K = "k"
static let COL_CONFIG_V = "v"
// Status values
static let STATUS_SCHEDULED = "scheduled"
static let STATUS_SHOWN = "shown"
static let STATUS_ERROR = "error"
static let STATUS_CANCELED = "canceled"
// MARK: - Properties
private var db: OpaquePointer?
private let path: String
// MARK: - Initialization
/**
* Initialize database with path
*
* @param path Database file path
*/
init(path: String) {
self.path = path
openDatabase()
}
/**
* Open database connection
*/
private func openDatabase() {
if sqlite3_open(path, &db) == SQLITE_OK {
print("\(Self.TAG): Database opened successfully at \(path)")
createTables()
configureDatabase()
} else {
print("\(Self.TAG): Error opening database: \(String(cString: sqlite3_errmsg(db)))")
}
}
/**
* Create database tables
*/
private func createTables() {
// Create notif_contents table
let createContentsTable = """
CREATE TABLE IF NOT EXISTS \(Self.TABLE_NOTIF_CONTENTS)(
\(Self.COL_CONTENTS_ID) INTEGER PRIMARY KEY AUTOINCREMENT,
\(Self.COL_CONTENTS_SLOT_ID) TEXT NOT NULL,
\(Self.COL_CONTENTS_PAYLOAD_JSON) TEXT NOT NULL,
\(Self.COL_CONTENTS_FETCHED_AT) INTEGER NOT NULL,
\(Self.COL_CONTENTS_ETAG) TEXT,
UNIQUE(\(Self.COL_CONTENTS_SLOT_ID), \(Self.COL_CONTENTS_FETCHED_AT))
);
"""
// Create notif_deliveries table
let createDeliveriesTable = """
CREATE TABLE IF NOT EXISTS \(Self.TABLE_NOTIF_DELIVERIES)(
\(Self.COL_DELIVERIES_ID) INTEGER PRIMARY KEY AUTOINCREMENT,
\(Self.COL_DELIVERIES_SLOT_ID) TEXT NOT NULL,
\(Self.COL_DELIVERIES_FIRE_AT) INTEGER NOT NULL,
\(Self.COL_DELIVERIES_DELIVERED_AT) INTEGER,
\(Self.COL_DELIVERIES_STATUS) TEXT NOT NULL DEFAULT '\(Self.STATUS_SCHEDULED)',
\(Self.COL_DELIVERIES_ERROR_CODE) TEXT,
\(Self.COL_DELIVERIES_ERROR_MESSAGE) TEXT
);
"""
// Create notif_config table
let createConfigTable = """
CREATE TABLE IF NOT EXISTS \(Self.TABLE_NOTIF_CONFIG)(
\(Self.COL_CONFIG_K) TEXT PRIMARY KEY,
\(Self.COL_CONFIG_V) TEXT NOT NULL
);
"""
// Create indexes
let createContentsIndex = """
CREATE INDEX IF NOT EXISTS notif_idx_contents_slot_time
ON \(Self.TABLE_NOTIF_CONTENTS)(\(Self.COL_CONTENTS_SLOT_ID), \(Self.COL_CONTENTS_FETCHED_AT) DESC);
"""
// Execute table creation
executeSQL(createContentsTable)
executeSQL(createDeliveriesTable)
executeSQL(createConfigTable)
executeSQL(createContentsIndex)
print("\(Self.TAG): Database tables created successfully")
}
/**
* Configure database settings
*/
private func configureDatabase() {
// Enable WAL mode
executeSQL("PRAGMA journal_mode=WAL")
// Set synchronous mode
executeSQL("PRAGMA synchronous=NORMAL")
// Set busy timeout
executeSQL("PRAGMA busy_timeout=5000")
// Enable foreign keys
executeSQL("PRAGMA foreign_keys=ON")
// Set user version
executeSQL("PRAGMA user_version=1")
print("\(Self.TAG): Database configured successfully")
}
/**
* Execute SQL statement
*
* @param sql SQL statement to execute
*/
func executeSQL(_ sql: String) {
var statement: OpaquePointer?
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK {
if sqlite3_step(statement) == SQLITE_DONE {
print("\(Self.TAG): SQL executed successfully: \(sql)")
} else {
print("\(Self.TAG): SQL execution failed: \(String(cString: sqlite3_errmsg(db)))")
}
} else {
print("\(Self.TAG): SQL preparation failed: \(String(cString: sqlite3_errmsg(db)))")
}
sqlite3_finalize(statement)
}
// MARK: - Public Methods
/**
* Close database connection
*/
func close() {
if sqlite3_close(db) == SQLITE_OK {
print("\(Self.TAG): Database closed successfully")
} else {
print("\(Self.TAG): Error closing database: \(String(cString: sqlite3_errmsg(db)))")
}
}
/**
* Get database path
*
* @return Database file path
*/
func getPath() -> String {
return path
}
/**
* Check if database is open
*
* @return true if database is open
*/
func isOpen() -> Bool {
return db != nil
}
/**
* Save notification content to database
*
* @param content Notification content to save
*/
func saveNotificationContent(_ content: NotificationContent) {
// TODO: Implement database persistence
// For Phase 1, storage uses UserDefaults primarily
print("\(Self.TAG): saveNotificationContent called for \(content.id)")
}
/**
* Delete notification content from database
*
* @param id Notification ID
*/
func deleteNotificationContent(id: String) {
// TODO: Implement database deletion
print("\(Self.TAG): deleteNotificationContent called for \(id)")
}
/**
* Clear all notifications from database
*/
func clearAllNotifications() {
// TODO: Implement database clearing
print("\(Self.TAG): clearAllNotifications called")
}
}