diff --git a/docs/CONSOLE_BUILD_GUIDE.md b/docs/CONSOLE_BUILD_GUIDE.md new file mode 100644 index 0000000..1157d5b --- /dev/null +++ b/docs/CONSOLE_BUILD_GUIDE.md @@ -0,0 +1,291 @@ +# Building Everything from Console + +**Author**: Matthew Raymer +**Date**: November 4, 2025 + +## Quick Start + +Build everything (plugin + iOS + Android): + +```bash +./scripts/build-all.sh +``` + +Build specific platform: + +```bash +./scripts/build-all.sh ios # iOS only +./scripts/build-all.sh android # Android only +./scripts/build-all.sh all # Everything (default) +``` + +## What Gets Built + +### 1. Plugin Build +- Compiles TypeScript to JavaScript +- Builds native iOS code (Swift) +- Builds native Android code (Kotlin/Java) +- Creates plugin frameworks/bundles + +### 2. Android Build +- Builds Android app (`android/app`) +- Creates debug APK +- Output: `android/app/build/outputs/apk/debug/app-debug.apk` + +### 3. iOS Build +- Installs CocoaPods dependencies +- Builds iOS app (`ios/App`) +- Creates simulator app bundle +- Output: `ios/App/build/derivedData/Build/Products/Debug-iphonesimulator/App.app` + +## Detailed Build Process + +### Step-by-Step Build + +```bash +# 1. Build plugin (TypeScript + Native) +./scripts/build-native.sh --platform all + +# 2. Build Android app +cd android +./gradlew :app:assembleDebug +cd .. + +# 3. Build iOS app +cd ios +pod install +cd App +xcodebuild -workspace App.xcworkspace \ + -scheme App \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO +``` + +### Platform-Specific Builds + +#### Android Only + +```bash +# Build plugin for Android +./scripts/build-native.sh --platform android + +# Build Android app +cd android +./gradlew :app:assembleDebug + +# Install on device/emulator +adb install app/build/outputs/apk/debug/app-debug.apk +``` + +#### iOS Only + +```bash +# Build plugin for iOS +./scripts/build-native.sh --platform ios + +# Install CocoaPods dependencies +cd ios +pod install + +# Build iOS app +cd App +xcodebuild -workspace App.xcworkspace \ + -scheme App \ + -configuration Debug \ + -sdk iphonesimulator \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO + +# Deploy to simulator (see deployment scripts) +../scripts/build-and-deploy-native-ios.sh +``` + +## Build Scripts + +### Main Build Script + +**`scripts/build-all.sh`** +- Builds plugin + iOS + Android +- Handles dependencies automatically +- Provides clear error messages + +### Platform-Specific Scripts + +**`scripts/build-native.sh`** +- Builds plugin only (TypeScript + native code) +- Supports `--platform ios`, `--platform android`, `--platform all` + +**`scripts/build-and-deploy-native-ios.sh`** +- Builds iOS plugin + app +- Deploys to simulator automatically +- Includes booting simulator and launching app + +**`test-apps/daily-notification-test/scripts/build-and-deploy-ios.sh`** +- Builds Vue 3 test app +- Syncs web assets +- Deploys to simulator + +## Build Outputs + +### Android + +``` +android/app/build/outputs/apk/debug/app-debug.apk +``` + +### iOS + +``` +ios/App/build/derivedData/Build/Products/Debug-iphonesimulator/App.app +``` + +### Plugin + +``` +ios/build/derivedData/Build/Products/*/DailyNotificationPlugin.framework +android/plugin/build/outputs/aar/plugin-release.aar +``` + +## Prerequisites + +### For All Platforms + +- Node.js and npm +- Git + +### For Android + +- Android SDK +- Java JDK (8 or higher) +- Gradle (or use Gradle wrapper) + +### For iOS + +- macOS +- Xcode Command Line Tools +- CocoaPods (`gem install cocoapods`) + +## Troubleshooting + +### Build Fails + +```bash +# Clean and rebuild +./scripts/build-native.sh --platform all --clean + +# Android: Clean Gradle cache +cd android && ./gradlew clean && cd .. + +# iOS: Clean Xcode build +cd ios/App && xcodebuild clean && cd ../.. +``` + +### Dependencies Out of Date + +```bash +# Update npm dependencies +npm install + +# Update CocoaPods +cd ios && pod update && cd .. + +# Update Android dependencies +cd android && ./gradlew --refresh-dependencies && cd .. +``` + +### iOS Project Not Found + +If `ios/App/App.xcworkspace` doesn't exist: + +```bash +# Initialize iOS app with Capacitor +cd ios +npx cap sync ios +pod install +``` + +### Android Build Issues + +```bash +# Verify Android SDK +echo $ANDROID_HOME + +# Clean build +cd android +./gradlew clean +./gradlew :app:assembleDebug +``` + +## CI/CD Integration + +### GitHub Actions Example + +```yaml +name: Build All Platforms + +on: [push, pull_request] + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + - name: Build Everything + run: ./scripts/build-all.sh all +``` + +### Android-Only CI + +```yaml +name: Build Android + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + - uses: actions/setup-java@v2 + - name: Build Android + run: ./scripts/build-all.sh android +``` + +## Verification + +After building, verify outputs: + +```bash +# Android APK exists +test -f android/app/build/outputs/apk/debug/app-debug.apk && echo "✓ Android APK" + +# iOS app bundle exists +test -d ios/App/build/derivedData/Build/Products/Debug-iphonesimulator/App.app && echo "✓ iOS app" + +# Plugin frameworks exist +test -d ios/build/derivedData/Build/Products/*/DailyNotificationPlugin.framework && echo "✓ iOS plugin" +test -f android/plugin/build/outputs/aar/plugin-release.aar && echo "✓ Android plugin" +``` + +## Next Steps + +After building: + +1. **Deploy Android**: `adb install android/app/build/outputs/apk/debug/app-debug.apk` +2. **Deploy iOS**: Use `scripts/build-and-deploy-native-ios.sh` +3. **Test**: Run plugin tests and verify functionality +4. **Debug**: Use platform-specific debugging tools + +## References + +- [Build Native Script](scripts/build-native.sh) +- [iOS Deployment Guide](docs/standalone-ios-simulator-guide.md) +- [Android Build Guide](BUILDING.md) + diff --git a/docs/VIEWING_BUILD_ERRORS.md b/docs/VIEWING_BUILD_ERRORS.md new file mode 100644 index 0000000..d7a87cb --- /dev/null +++ b/docs/VIEWING_BUILD_ERRORS.md @@ -0,0 +1,199 @@ +# Viewing Build Errors - Full Output Guide + +**Author**: Matthew Raymer +**Date**: November 4, 2025 + +## Quick Methods to See Full Errors + +### Method 1: Run Build Script and Check Log Files + +```bash +# Run the build script +./scripts/build-native.sh --platform ios + +# If it fails, check the log files: +cat /tmp/xcodebuild_device.log # Device build errors +cat /tmp/xcodebuild_simulator.log # Simulator build errors + +# View only errors: +grep -E "(error:|warning:)" /tmp/xcodebuild_simulator.log +``` + +### Method 2: Run xcodebuild Directly (No Script Filtering) + +```bash +# Build for simulator with full output +cd ios +xcodebuild -workspace DailyNotificationPlugin.xcworkspace \ + -scheme DailyNotificationPlugin \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ + | tee build-output.log + +# Then view errors: +grep -E "(error:|warning:)" build-output.log +``` + +### Method 3: Redirect to File and View + +```bash +# Save full output to file +./scripts/build-native.sh --platform ios 2>&1 | tee build-full.log + +# View errors +grep -E "(error:|warning:|ERROR|FAILED)" build-full.log + +# View last 100 lines +tail -100 build-full.log +``` + +### Method 4: Use xcodebuild with Verbose Output + +```bash +cd ios + +# Build with verbose output +xcodebuild -workspace DailyNotificationPlugin.xcworkspace \ + -scheme DailyNotificationPlugin \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ + -verbose \ + 2>&1 | tee build-verbose.log + +# Extract just errors +grep -E "^error:" build-verbose.log | head -50 +``` + +## Filtering Output + +### Show Only Errors + +```bash +# From log file +grep -E "^error:" /tmp/xcodebuild_simulator.log + +# From live output +./scripts/build-native.sh --platform ios 2>&1 | grep -E "(error:|ERROR)" +``` + +### Show Errors with Context (5 lines before/after) + +```bash +grep -E "(error:|warning:)" -A 5 -B 5 /tmp/xcodebuild_simulator.log | head -100 +``` + +### Count Errors + +```bash +grep -c "error:" /tmp/xcodebuild_simulator.log +``` + +### Show Errors Grouped by File + +```bash +grep "error:" /tmp/xcodebuild_simulator.log | cut -d: -f1-3 | sort | uniq -c | sort -rn +``` + +## Common Error Patterns + +### Swift Compilation Errors + +```bash +# Find all Swift compilation errors +grep -E "\.swift.*error:" /tmp/xcodebuild_simulator.log + +# Find missing type errors +grep -E "cannot find type.*in scope" /tmp/xcodebuild_simulator.log + +# Find import errors +grep -E "No such module|Cannot find.*in scope" /tmp/xcodebuild_simulator.log +``` + +### CocoaPods Errors + +```bash +# Find CocoaPods errors +grep -E "(pod|CocoaPods)" /tmp/xcodebuild_simulator.log -i +``` + +### Build System Errors + +```bash +# Find build system errors +grep -E "(BUILD FAILED|error:)" /tmp/xcodebuild_simulator.log +``` + +## Debugging Tips + +### See Full Command Being Run + +Add `set -x` at the top of the script, or run: + +```bash +bash -x ./scripts/build-native.sh --platform ios 2>&1 | tee build-debug.log +``` + +### Check Exit Codes + +```bash +./scripts/build-native.sh --platform ios +echo "Exit code: $?" +``` + +### View Build Settings + +```bash +cd ios +xcodebuild -workspace DailyNotificationPlugin.xcworkspace \ + -scheme DailyNotificationPlugin \ + -showBuildSettings 2>&1 | grep -E "(SWIFT|FRAMEWORK|HEADER)" +``` + +## Example: Full Debug Session + +```bash +# 1. Run build and save everything +./scripts/build-native.sh --platform ios 2>&1 | tee build-full.log + +# 2. Check exit code +echo "Build exit code: $?" + +# 3. Extract errors +echo "=== ERRORS ===" > errors.txt +grep -E "(error:|ERROR)" build-full.log >> errors.txt + +# 4. Extract warnings +echo "=== WARNINGS ===" >> errors.txt +grep -E "(warning:|WARNING)" build-full.log >> errors.txt + +# 5. View errors file +cat errors.txt + +# 6. Check log files created by script +ls -lh /tmp/xcodebuild*.log +``` + +## Quick Reference + +```bash +# Most common: View simulator build errors +cat /tmp/xcodebuild_simulator.log | grep -E "(error:|warning:)" | head -30 + +# View full build log +cat /tmp/xcodebuild_simulator.log | less + +# Search for specific error +grep -i "cannot find type" /tmp/xcodebuild_simulator.log + +# Count errors by type +grep "error:" /tmp/xcodebuild_simulator.log | cut -d: -f4 | sort | uniq -c | sort -rn +``` + diff --git a/docs/ios-native-interface.md b/docs/ios-native-interface.md new file mode 100644 index 0000000..a7e480b --- /dev/null +++ b/docs/ios-native-interface.md @@ -0,0 +1,185 @@ +# iOS Native Interface Structure + +**Author**: Matthew Raymer +**Date**: November 4, 2025 + +## Overview + +The iOS native interface mirrors the Android structure, providing the same functionality through iOS-specific implementations. + +## Directory Structure + +``` +ios/App/App/ +├── AppDelegate.swift # Application lifecycle (equivalent to PluginApplication.java) +├── ViewController.swift # Main view controller (equivalent to MainActivity.java) +├── SceneDelegate.swift # Scene-based lifecycle (iOS 13+) +├── Info.plist # App configuration (equivalent to AndroidManifest.xml) +├── capacitor.config.json # Capacitor configuration +├── config.xml # Cordova compatibility +└── public/ # Web assets (equivalent to assets/public/) + ├── index.html + ├── capacitor.js + └── capacitor_plugins.js +``` + +## File Descriptions + +### AppDelegate.swift + +**Purpose**: Application lifecycle management +**Equivalent**: `PluginApplication.java` on Android + +- Handles app lifecycle events (launch, background, foreground, termination) +- Registers for push notifications +- Handles URL schemes and universal links +- Initializes plugin demo fetcher (equivalent to Android's `PluginApplication.onCreate()`) + +**Key Methods**: +- `application(_:didFinishLaunchingWithOptions:)` - App initialization +- `applicationDidEnterBackground(_:)` - Background handling +- `applicationWillEnterForeground(_:)` - Foreground handling +- `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)` - Push notification registration + +### ViewController.swift + +**Purpose**: Main view controller extending Capacitor's bridge +**Equivalent**: `MainActivity.java` on Android + +- Extends `CAPBridgeViewController` (Capacitor's bridge view controller) +- Initializes plugin and registers native fetcher +- Handles view lifecycle events + +**Key Methods**: +- `viewDidLoad()` - View initialization +- `initializePlugin()` - Plugin registration (equivalent to Android's plugin registration) + +### SceneDelegate.swift + +**Purpose**: Scene-based lifecycle management (iOS 13+) +**Equivalent**: None on Android (iOS-specific) + +- Handles scene creation and lifecycle +- Manages window and view controller setup +- Required for modern iOS apps using scene-based architecture + +### Info.plist + +**Purpose**: App configuration and permissions +**Equivalent**: `AndroidManifest.xml` on Android + +**Key Entries**: +- `CFBundleIdentifier` - App bundle ID +- `NSUserNotificationsUsageDescription` - Notification permission description +- `UIBackgroundModes` - Background modes (fetch, processing, remote-notification) +- `BGTaskSchedulerPermittedIdentifiers` - Background task identifiers +- `UIApplicationSceneManifest` - Scene configuration + +## Comparison: Android vs iOS + +| Component | Android | iOS | +|-----------|---------|-----| +| **Application Class** | `PluginApplication.java` | `AppDelegate.swift` | +| **Main Activity** | `MainActivity.java` | `ViewController.swift` | +| **Config File** | `AndroidManifest.xml` | `Info.plist` | +| **Web Assets** | `assets/public/` | `public/` | +| **Lifecycle** | `onCreate()`, `onResume()`, etc. | `viewDidLoad()`, `viewWillAppear()`, etc. | +| **Bridge** | `BridgeActivity` | `CAPBridgeViewController` | + +## Plugin Registration + +### Android + +```java +public class PluginApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + NativeNotificationContentFetcher demoFetcher = new DemoNativeFetcher(); + DailyNotificationPlugin.setNativeFetcher(demoFetcher); + } +} +``` + +### iOS + +```swift +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Plugin registration happens in ViewController after Capacitor bridge is initialized + return true + } +} + +class ViewController: CAPBridgeViewController { + override func viewDidLoad() { + super.viewDidLoad() + initializePlugin() + } + + private func initializePlugin() { + // Register demo native fetcher if implementing SPI + // DailyNotificationPlugin.setNativeFetcher(DemoNativeFetcher()) + } +} +``` + +## Build Process + +1. **Swift Compilation**: Compiles `AppDelegate.swift`, `ViewController.swift`, `SceneDelegate.swift` +2. **Capacitor Integration**: Links with Capacitor framework and plugin +3. **Web Assets**: Copies `public/` directory to app bundle +4. **Info.plist**: Processes app configuration and permissions +5. **App Bundle**: Creates `.app` bundle for installation + +## Permissions + +### Android (AndroidManifest.xml) + +```xml + + + +``` + +### iOS (Info.plist) + +```xml +NSUserNotificationsUsageDescription +This app uses notifications to deliver daily updates and reminders. + +UIBackgroundModes + + background-fetch + background-processing + remote-notification + +``` + +## Background Tasks + +### Android + +- Uses `WorkManager` and `AlarmManager` +- Declared in `AndroidManifest.xml` receivers + +### iOS + +- Uses `BGTaskScheduler` and `UNUserNotificationCenter` +- Declared in `Info.plist` with `BGTaskSchedulerPermittedIdentifiers` + +## Next Steps + +1. Ensure Xcode project includes these Swift files +2. Configure build settings in Xcode project +3. Add app icons and launch screen +4. Test plugin registration and native fetcher +5. Verify background tasks work correctly + +## References + +- [Capacitor iOS Documentation](https://capacitorjs.com/docs/ios) +- [iOS App Lifecycle](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle) +- [Background Tasks](https://developer.apple.com/documentation/backgroundtasks) + diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift new file mode 100644 index 0000000..fe22ca7 --- /dev/null +++ b/ios/App/App/AppDelegate.swift @@ -0,0 +1,106 @@ +// +// AppDelegate.swift +// DailyNotification Test App +// +// Application delegate for the Daily Notification Plugin demo app. +// Registers the native content fetcher SPI implementation. +// +// @author Matthew Raymer +// @version 1.0.0 +// @created 2025-11-04 +// + +import UIKit +import Capacitor + +/** + * Application delegate for Daily Notification Plugin demo app + * Equivalent to PluginApplication.java on Android + */ +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Initialize Daily Notification Plugin demo fetcher + // Note: This is called before Capacitor bridge is initialized + // Plugin registration happens in ViewController + + print("AppDelegate: Initializing Daily Notification Plugin demo app") + + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Pause ongoing tasks + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Release resources when app enters background + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Restore resources when app enters foreground + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart paused tasks + } + + func applicationWillTerminate(_ application: UIApplication) { + // Save data before app terminates + } + + // MARK: - URL Scheme Handling + + func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + // Handle URL schemes (e.g., deep links) + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } + + // MARK: - Universal Links + + func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + // Handle universal links + return ApplicationDelegateProxy.shared.application( + application, + continue: userActivity, + restorationHandler: restorationHandler + ) + } + + // MARK: - Push Notifications + + func application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + // Handle device token registration + NotificationCenter.default.post( + name: Notification.Name("didRegisterForRemoteNotifications"), + object: nil, + userInfo: ["deviceToken": deviceToken] + ) + } + + func application( + _ application: UIApplication, + didFailToRegisterForRemoteNotificationsWithError error: Error + ) { + // Handle registration failure + print("AppDelegate: Failed to register for remote notifications: \(error)") + } +} + diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist new file mode 100644 index 0000000..3dfcc49 --- /dev/null +++ b/ios/App/App/Info.plist @@ -0,0 +1,119 @@ + + + + + + CFBundleDisplayName + DailyNotification Test + + + CFBundleIdentifier + com.timesafari.dailynotification + + + CFBundleName + DailyNotification Test App + + + CFBundleShortVersionString + 1.0.0 + + + CFBundleVersion + 1 + + + LSMinimumSystemVersion + 13.0 + + + UIDeviceFamily + + 1 + 2 + + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + + UIStatusBarStyle + UIStatusBarStyleDefault + + + UIStatusBarHidden + + + + UILaunchStoryboardName + LaunchScreen + + + NSUserNotificationsUsageDescription + This app uses notifications to deliver daily updates and reminders. + + + UIBackgroundModes + + background-fetch + background-processing + remote-notification + + + + BGTaskSchedulerPermittedIdentifiers + + com.timesafari.dailynotification.fetch + com.timesafari.dailynotification.notify + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + + UIApplicationExitsOnSuspend + + + + diff --git a/ios/App/App/SceneDelegate.swift b/ios/App/App/SceneDelegate.swift new file mode 100644 index 0000000..4e012a6 --- /dev/null +++ b/ios/App/App/SceneDelegate.swift @@ -0,0 +1,61 @@ +// +// SceneDelegate.swift +// DailyNotification Test App +// +// Scene delegate for iOS 13+ scene-based lifecycle. +// Handles scene creation and lifecycle events. +// +// @author Matthew Raymer +// @version 1.0.0 +// @created 2025-11-04 +// + +import UIKit + +/** + * Scene delegate for iOS 13+ scene-based lifecycle + * Required for modern iOS apps using scene-based architecture + */ +@available(iOS 13.0, *) +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + // Called when a new scene session is being created + guard let windowScene = (scene as? UIWindowScene) else { return } + + let window = UIWindow(windowScene: windowScene) + self.window = window + + // Create and configure the view controller + let viewController = ViewController() + window.rootViewController = viewController + window.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called when the scene is being released by the system + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from inactive to active state + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from active to inactive state + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called when the scene is about to move from background to foreground + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called when the scene has moved from background to foreground + } +} + diff --git a/ios/App/App/ViewController.swift b/ios/App/App/ViewController.swift new file mode 100644 index 0000000..79bbe32 --- /dev/null +++ b/ios/App/App/ViewController.swift @@ -0,0 +1,69 @@ +// +// ViewController.swift +// DailyNotification Test App +// +// Main view controller for the Daily Notification Plugin demo app. +// Equivalent to MainActivity.java on Android - extends Capacitor's bridge. +// +// @author Matthew Raymer +// @version 1.0.0 +// @created 2025-11-04 +// + +import UIKit +import Capacitor + +/** + * Main view controller extending Capacitor's bridge view controller + * Equivalent to MainActivity extends BridgeActivity on Android + */ +class ViewController: CAPBridgeViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Initialize Daily Notification Plugin demo fetcher + // This is called after Capacitor bridge is initialized + initializePlugin() + } + + /** + * Initialize plugin and register native fetcher + * Equivalent to PluginApplication.onCreate() on Android + */ + private func initializePlugin() { + print("ViewController: Initializing Daily Notification Plugin") + + // Note: Plugin registration happens automatically via Capacitor + // Native fetcher registration can be done here if needed + + // Example: Register demo native fetcher (if implementing SPI) + // DailyNotificationPlugin.setNativeFetcher(DemoNativeFetcher()) + + print("ViewController: Daily Notification Plugin initialized") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + + // MARK: - Memory Management + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated + } +} + diff --git a/ios/Plugin/DailyNotificationBackgroundTaskManager.swift b/ios/Plugin/DailyNotificationBackgroundTaskManager.swift index b83d2d5..d63c97c 100644 --- a/ios/Plugin/DailyNotificationBackgroundTaskManager.swift +++ b/ios/Plugin/DailyNotificationBackgroundTaskManager.swift @@ -292,11 +292,17 @@ class DailyNotificationBackgroundTaskManager { // Parse new content let newContent = try JSONSerialization.jsonObject(with: data) as? [String: Any] - // Update notification with new content - var updatedNotification = notification - updatedNotification.payload = newContent - updatedNotification.fetchedAt = Date().timeIntervalSince1970 * 1000 - updatedNotification.etag = response.allHeaderFields["ETag"] as? String + // Create new notification instance with updated content + let updatedNotification = NotificationContent( + id: notification.id, + title: notification.title, + body: notification.body, + scheduledTime: notification.scheduledTime, + fetchedAt: Date().timeIntervalSince1970 * 1000, + url: notification.url, + payload: newContent, + etag: response.allHeaderFields["ETag"] as? String + ) // Check TTL before storing if ttlEnforcer.validateBeforeArming(updatedNotification) { @@ -335,8 +341,16 @@ class DailyNotificationBackgroundTaskManager { // Update ETag if provided if let etag = response.allHeaderFields["ETag"] as? String { - var updatedNotification = notification - updatedNotification.etag = etag + let updatedNotification = NotificationContent( + id: notification.id, + title: notification.title, + body: notification.body, + scheduledTime: notification.scheduledTime, + fetchedAt: notification.fetchedAt, + url: notification.url, + payload: notification.payload, + etag: etag + ) storeUpdatedContent(updatedNotification) { success in completion(success) } diff --git a/ios/Plugin/DailyNotificationBackgroundTasks.swift b/ios/Plugin/DailyNotificationBackgroundTasks.swift index d2c18f4..8b71a1f 100644 --- a/ios/Plugin/DailyNotificationBackgroundTasks.swift +++ b/ios/Plugin/DailyNotificationBackgroundTasks.swift @@ -21,7 +21,7 @@ import CoreData */ extension DailyNotificationPlugin { - private func handleBackgroundFetch(task: BGAppRefreshTask) { + func handleBackgroundFetch(task: BGAppRefreshTask) { print("DNP-FETCH-START: Background fetch task started") task.expirationHandler = { @@ -52,7 +52,7 @@ extension DailyNotificationPlugin { } } - private func handleBackgroundNotify(task: BGProcessingTask) { + func handleBackgroundNotify(task: BGProcessingTask) { print("DNP-NOTIFY-START: Background notify task started") task.expirationHandler = { @@ -124,7 +124,7 @@ extension DailyNotificationPlugin { print("DNP-CACHE-STORE: Content stored in Core Data") } - private func getLatestContent() async throws -> [String: Any]? { + func getLatestContent() async throws -> [String: Any]? { let context = persistenceController.container.viewContext let request: NSFetchRequest = ContentCache.fetchRequest() request.sortDescriptors = [NSSortDescriptor(keyPath: \ContentCache.fetchedAt, ascending: false)] diff --git a/ios/Plugin/DailyNotificationCallbacks.swift b/ios/Plugin/DailyNotificationCallbacks.swift index d16c1ea..530c2c5 100644 --- a/ios/Plugin/DailyNotificationCallbacks.swift +++ b/ios/Plugin/DailyNotificationCallbacks.swift @@ -7,6 +7,7 @@ // import Foundation +import Capacitor import CoreData /** @@ -108,7 +109,7 @@ extension DailyNotificationPlugin { // MARK: - Private Callback Implementation - private func fireCallbacks(eventType: String, payload: [String: Any]) async throws { + func fireCallbacks(eventType: String, payload: [String: Any]) async throws { // Get registered callbacks from Core Data let context = persistenceController.container.viewContext let request: NSFetchRequest = Callback.fetchRequest() @@ -246,7 +247,7 @@ extension DailyNotificationPlugin { } } - private func getHealthStatus() async throws -> [String: Any] { + func getHealthStatus() async throws -> [String: Any] { let context = persistenceController.container.viewContext // Get next runs (simplified) diff --git a/ios/Plugin/DailyNotificationETagManager.swift b/ios/Plugin/DailyNotificationETagManager.swift index 9356cea..f575b0c 100644 --- a/ios/Plugin/DailyNotificationETagManager.swift +++ b/ios/Plugin/DailyNotificationETagManager.swift @@ -69,7 +69,7 @@ class DailyNotificationETagManager { // Load ETag cache from storage loadETagCache() - logger.debug(TAG, "ETagManager initialized with \(etagCache.count) cached ETags") + logger.log(.debug, "ETagManager initialized with \(etagCache.count) cached ETags") } // MARK: - ETag Cache Management @@ -79,14 +79,14 @@ class DailyNotificationETagManager { */ private func loadETagCache() { do { - logger.debug(TAG, "Loading ETag cache from storage") + logger.log(.debug, "Loading ETag cache from storage") // This would typically load from SQLite or UserDefaults // For now, we'll start with an empty cache - logger.debug(TAG, "ETag cache loaded from storage") + logger.log(.debug, "ETag cache loaded from storage") } catch { - logger.error(TAG, "Error loading ETag cache: \(error)") + logger.log(.error, "Error loading ETag cache: \(error)") } } @@ -95,14 +95,14 @@ class DailyNotificationETagManager { */ private func saveETagCache() { do { - logger.debug(TAG, "Saving ETag cache to storage") + logger.log(.debug, "Saving ETag cache to storage") // This would typically save to SQLite or UserDefaults // For now, we'll just log the action - logger.debug(TAG, "ETag cache saved to storage") + logger.log(.debug, "ETag cache saved to storage") } catch { - logger.error(TAG, "Error saving ETag cache: \(error)") + logger.log(.error, "Error saving ETag cache: \(error)") } } @@ -130,7 +130,7 @@ class DailyNotificationETagManager { */ func setETag(for url: String, etag: String) { do { - logger.debug(TAG, "Setting ETag for \(url): \(etag)") + logger.log(.debug, "Setting ETag for \(url): \(etag)") let info = ETagInfo(etag: etag, timestamp: Date()) @@ -139,10 +139,10 @@ class DailyNotificationETagManager { self.saveETagCache() } - logger.debug(TAG, "ETag set successfully") + logger.log(.debug, "ETag set successfully") } catch { - logger.error(TAG, "Error setting ETag: \(error)") + logger.log(.error, "Error setting ETag: \(error)") } } @@ -153,17 +153,17 @@ class DailyNotificationETagManager { */ func removeETag(for url: String) { do { - logger.debug(TAG, "Removing ETag for \(url)") + logger.log(.debug, "Removing ETag for \(url)") cacheQueue.async(flags: .barrier) { self.etagCache.removeValue(forKey: url) self.saveETagCache() } - logger.debug(TAG, "ETag removed successfully") + logger.log(.debug, "ETag removed successfully") } catch { - logger.error(TAG, "Error removing ETag: \(error)") + logger.log(.error, "Error removing ETag: \(error)") } } @@ -172,17 +172,17 @@ class DailyNotificationETagManager { */ func clearETags() { do { - logger.debug(TAG, "Clearing all ETags") + logger.log(.debug, "Clearing all ETags") cacheQueue.async(flags: .barrier) { self.etagCache.removeAll() self.saveETagCache() } - logger.debug(TAG, "All ETags cleared") + logger.log(.debug, "All ETags cleared") } catch { - logger.error(TAG, "Error clearing ETags: \(error)") + logger.log(.error, "Error clearing ETags: \(error)") } } @@ -196,7 +196,7 @@ class DailyNotificationETagManager { */ func makeConditionalRequest(to url: String) -> ConditionalRequestResult { do { - logger.debug(TAG, "Making conditional request to \(url)") + logger.log(.debug, "Making conditional request to \(url)") // Get cached ETag let etag = getETag(for: url) @@ -212,16 +212,33 @@ class DailyNotificationETagManager { // Set conditional headers if let etag = etag { request.setValue(etag, forHTTPHeaderField: DailyNotificationETagManager.HEADER_IF_NONE_MATCH) - logger.debug(TAG, "Added If-None-Match header: \(etag)") + logger.log(.debug, "Added If-None-Match header: \(etag)") } // Set user agent request.setValue("DailyNotificationPlugin/1.0.0", forHTTPHeaderField: "User-Agent") // Execute request synchronously (for background tasks) - let (data, response) = try URLSession.shared.data(for: request) + let semaphore = DispatchSemaphore(value: 0) + var resultData: Data? + var resultResponse: URLResponse? + var resultError: Error? + + URLSession.shared.dataTask(with: request) { data, response, error in + resultData = data + resultResponse = response + resultError = error + semaphore.signal() + }.resume() + + _ = semaphore.wait(timeout: .now() + DailyNotificationETagManager.REQUEST_TIMEOUT_SECONDS) + + if let error = resultError { + throw error + } - guard let httpResponse = response as? HTTPURLResponse else { + guard let data = resultData, + let httpResponse = resultResponse as? HTTPURLResponse else { return ConditionalRequestResult.error("Invalid response type") } @@ -231,12 +248,12 @@ class DailyNotificationETagManager { // Update metrics metrics.recordRequest(url: url, responseCode: httpResponse.statusCode, fromCache: result.isFromCache) - logger.info(TAG, "Conditional request completed: \(httpResponse.statusCode) (cached: \(result.isFromCache))") + logger.log(.info, "Conditional request completed: \(httpResponse.statusCode) (cached: \(result.isFromCache))") return result } catch { - logger.error(TAG, "Error making conditional request: \(error)") + logger.log(.error, "Error making conditional request: \(error)") metrics.recordError(url: url, error: error.localizedDescription) return ConditionalRequestResult.error(error.localizedDescription) } @@ -254,20 +271,20 @@ class DailyNotificationETagManager { do { switch response.statusCode { case DailyNotificationETagManager.HTTP_NOT_MODIFIED: - logger.debug(TAG, "304 Not Modified - using cached content") + logger.log(.debug, "304 Not Modified - using cached content") return ConditionalRequestResult.notModified() case DailyNotificationETagManager.HTTP_OK: - logger.debug(TAG, "200 OK - new content available") + logger.log(.debug, "200 OK - new content available") return handleOKResponse(response, data: data, url: url) default: - logger.warning(TAG, "Unexpected response code: \(response.statusCode)") + logger.log(.warning, "Unexpected response code: \(response.statusCode)") return ConditionalRequestResult.error("Unexpected response code: \(response.statusCode)") } } catch { - logger.error(TAG, "Error handling response: \(error)") + logger.log(.error, "Error handling response: \(error)") return ConditionalRequestResult.error(error.localizedDescription) } } @@ -298,7 +315,7 @@ class DailyNotificationETagManager { return ConditionalRequestResult.success(content: content, etag: newETag) } catch { - logger.error(TAG, "Error handling OK response: \(error)") + logger.log(.error, "Error handling OK response: \(error)") return ConditionalRequestResult.error(error.localizedDescription) } } @@ -319,7 +336,7 @@ class DailyNotificationETagManager { */ func resetMetrics() { metrics.reset() - logger.debug(TAG, "Network metrics reset") + logger.log(.debug, "Network metrics reset") } // MARK: - Cache Management @@ -329,7 +346,7 @@ class DailyNotificationETagManager { */ func cleanExpiredETags() { do { - logger.debug(TAG, "Cleaning expired ETags") + logger.log(.debug, "Cleaning expired ETags") let initialSize = etagCache.count @@ -341,11 +358,11 @@ class DailyNotificationETagManager { if initialSize != finalSize { saveETagCache() - logger.info(TAG, "Cleaned \(initialSize - finalSize) expired ETags") + logger.log(.info, "Cleaned \(initialSize - finalSize) expired ETags") } } catch { - logger.error(TAG, "Error cleaning expired ETags: \(error)") + logger.log(.error, "Error cleaning expired ETags: \(error)") } } diff --git a/ios/Plugin/DailyNotificationErrorHandler.swift b/ios/Plugin/DailyNotificationErrorHandler.swift index 018d8b2..c9119fe 100644 --- a/ios/Plugin/DailyNotificationErrorHandler.swift +++ b/ios/Plugin/DailyNotificationErrorHandler.swift @@ -68,7 +68,7 @@ class DailyNotificationErrorHandler { self.logger = logger self.config = ErrorConfiguration() - logger.debug(DailyNotificationErrorHandler.TAG, "ErrorHandler initialized with max retries: \(config.maxRetries)") + logger.log(.debug, "ErrorHandler initialized with max retries: \(config.maxRetries)") } /** @@ -81,7 +81,7 @@ class DailyNotificationErrorHandler { self.logger = logger self.config = config - logger.debug(DailyNotificationErrorHandler.TAG, "ErrorHandler initialized with max retries: \(config.maxRetries)") + logger.log(.debug, "ErrorHandler initialized with max retries: \(config.maxRetries)") } // MARK: - Error Handling @@ -96,7 +96,7 @@ class DailyNotificationErrorHandler { */ func handleError(operationId: String, error: Error, retryable: Bool) -> ErrorResult { do { - logger.debug(DailyNotificationErrorHandler.TAG, "Handling error for operation: \(operationId)") + logger.log(.debug, "Handling error for operation: \(operationId)") // Categorize error let errorInfo = categorizeError(error) @@ -112,7 +112,7 @@ class DailyNotificationErrorHandler { } } catch { - logger.error(DailyNotificationErrorHandler.TAG, "Error in error handler: \(error)") + logger.log(.error, "Error in error handler: \(error)") return ErrorResult.fatal(message: "Error handler failure: \(error.localizedDescription)") } } @@ -127,7 +127,7 @@ class DailyNotificationErrorHandler { */ func handleError(operationId: String, error: Error, retryConfig: RetryConfiguration) -> ErrorResult { do { - logger.debug(DailyNotificationErrorHandler.TAG, "Handling error with custom retry config for operation: \(operationId)") + logger.log(.debug, "Handling error with custom retry config for operation: \(operationId)") // Categorize error let errorInfo = categorizeError(error) @@ -143,7 +143,7 @@ class DailyNotificationErrorHandler { } } catch { - logger.error(DailyNotificationErrorHandler.TAG, "Error in error handler with custom config: \(error)") + logger.log(.error, "Error in error handler with custom config: \(error)") return ErrorResult.fatal(message: "Error handler failure: \(error.localizedDescription)") } } @@ -170,11 +170,11 @@ class DailyNotificationErrorHandler { timestamp: Date() ) - logger.debug(DailyNotificationErrorHandler.TAG, "Error categorized: \(errorInfo)") + logger.log(.debug, "Error categorized: \(errorInfo)") return errorInfo } catch { - logger.error(DailyNotificationErrorHandler.TAG, "Error during categorization: \(error)") + logger.log(.error, "Error during categorization: \(error)") return ErrorInfo( error: error, category: .unknown, @@ -299,7 +299,7 @@ class DailyNotificationErrorHandler { private func shouldRetry(operationId: String, errorInfo: ErrorInfo, retryConfig: RetryConfiguration?) -> Bool { do { // Get retry state - var state: RetryState + var state: RetryState! retryQueue.sync { if retryStates[operationId] == nil { retryStates[operationId] = RetryState() @@ -310,18 +310,18 @@ class DailyNotificationErrorHandler { // Check retry limits let maxRetries = retryConfig?.maxRetries ?? config.maxRetries if state.attemptCount >= maxRetries { - logger.debug(DailyNotificationErrorHandler.TAG, "Max retries exceeded for operation: \(operationId)") + logger.log(.debug, "Max retries exceeded for operation: \(operationId)") return false } // Check if error is retryable based on category let isRetryable = isErrorRetryable(errorInfo.category) - logger.debug(DailyNotificationErrorHandler.TAG, "Should retry: \(isRetryable) (attempt: \(state.attemptCount)/\(maxRetries))") + logger.log(.debug, "Should retry: \(isRetryable) (attempt: \(state.attemptCount)/\(maxRetries))") return isRetryable } catch { - logger.error(DailyNotificationErrorHandler.TAG, "Error checking retry eligibility: \(error)") + logger.log(.error, "Error checking retry eligibility: \(error)") return false } } @@ -336,7 +336,7 @@ class DailyNotificationErrorHandler { switch category { case .network, .storage: return true - case .permission, .configuration, .system, .unknown: + case .permission, .configuration, .system, .unknown, .scheduling: return false } } @@ -362,8 +362,11 @@ class DailyNotificationErrorHandler { */ private func handleRetryableError(operationId: String, errorInfo: ErrorInfo, retryConfig: RetryConfiguration?) -> ErrorResult { do { - var state: RetryState + var state: RetryState! retryQueue.sync { + if retryStates[operationId] == nil { + retryStates[operationId] = RetryState() + } state = retryStates[operationId]! state.attemptCount += 1 } @@ -372,12 +375,12 @@ class DailyNotificationErrorHandler { let delay = calculateRetryDelay(attemptCount: state.attemptCount, retryConfig: retryConfig) state.nextRetryTime = Date().addingTimeInterval(delay) - logger.info(DailyNotificationErrorHandler.TAG, "Retryable error handled - retry in \(delay)s (attempt \(state.attemptCount))") + logger.log(.info, "Retryable error handled - retry in \(delay)s (attempt \(state.attemptCount))") return ErrorResult.retryable(errorInfo: errorInfo, retryDelaySeconds: delay, attemptCount: state.attemptCount) } catch { - logger.error(DailyNotificationErrorHandler.TAG, "Error handling retryable error: \(error)") + logger.log(.error, "Error handling retryable error: \(error)") return ErrorResult.fatal(message: "Retry handling failure: \(error.localizedDescription)") } } @@ -391,7 +394,7 @@ class DailyNotificationErrorHandler { */ private func handleNonRetryableError(operationId: String, errorInfo: ErrorInfo) -> ErrorResult { do { - logger.warning(DailyNotificationErrorHandler.TAG, "Non-retryable error handled for operation: \(operationId)") + logger.log(.warning, "Non-retryable error handled for operation: \(operationId)") // Clean up retry state retryQueue.async(flags: .barrier) { @@ -401,7 +404,7 @@ class DailyNotificationErrorHandler { return ErrorResult.fatal(errorInfo: errorInfo) } catch { - logger.error(DailyNotificationErrorHandler.TAG, "Error handling non-retryable error: \(error)") + logger.log(.error, "Error handling non-retryable error: \(error)") return ErrorResult.fatal(message: "Non-retryable error handling failure: \(error.localizedDescription)") } } @@ -429,11 +432,11 @@ class DailyNotificationErrorHandler { let jitter = delay * 0.1 * Double.random(in: 0...1) delay += jitter - logger.debug(DailyNotificationErrorHandler.TAG, "Calculated retry delay: \(delay)s (attempt \(attemptCount))") + logger.log(.debug, "Calculated retry delay: \(delay)s (attempt \(attemptCount))") return delay } catch { - logger.error(DailyNotificationErrorHandler.TAG, "Error calculating retry delay: \(error)") + logger.log(.error, "Error calculating retry delay: \(error)") return config.baseDelaySeconds } } @@ -454,7 +457,7 @@ class DailyNotificationErrorHandler { */ func resetMetrics() { metrics.reset() - logger.debug(DailyNotificationErrorHandler.TAG, "Error metrics reset") + logger.log(.debug, "Error metrics reset") } /** @@ -487,7 +490,7 @@ class DailyNotificationErrorHandler { retryQueue.async(flags: .barrier) { self.retryStates.removeAll() } - logger.debug(DailyNotificationErrorHandler.TAG, "Retry states cleared") + logger.log(.debug, "Retry states cleared") } // MARK: - Data Classes diff --git a/ios/Plugin/DailyNotificationPerformanceOptimizer.swift b/ios/Plugin/DailyNotificationPerformanceOptimizer.swift index 1016a62..13376af 100644 --- a/ios/Plugin/DailyNotificationPerformanceOptimizer.swift +++ b/ios/Plugin/DailyNotificationPerformanceOptimizer.swift @@ -75,7 +75,7 @@ class DailyNotificationPerformanceOptimizer { // Start performance monitoring startPerformanceMonitoring() - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "PerformanceOptimizer initialized") + logger.log(.debug, "PerformanceOptimizer initialized") } // MARK: - Database Optimization @@ -85,7 +85,7 @@ class DailyNotificationPerformanceOptimizer { */ func optimizeDatabase() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Optimizing database performance") + logger.log(.debug, "Optimizing database performance") // Add database indexes addDatabaseIndexes() @@ -99,10 +99,10 @@ class DailyNotificationPerformanceOptimizer { // Analyze database performance analyzeDatabasePerformance() - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Database optimization completed") + logger.log(.info, "Database optimization completed") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error optimizing database: \(error)") + logger.log(.error, "Error optimizing database: \(error)") } } @@ -111,22 +111,22 @@ class DailyNotificationPerformanceOptimizer { */ private func addDatabaseIndexes() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Adding database indexes for query optimization") + logger.log(.debug, "Adding database indexes for query optimization") - // Add indexes for common queries - try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_contents_slot_time ON notif_contents(slot_id, fetched_at DESC)") - try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_deliveries_status ON notif_deliveries(status)") - try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_deliveries_fire_time ON notif_deliveries(fire_at)") - try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_config_key ON notif_config(k)") + // TODO: Implement database index creation when execSQL is available + // try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_contents_slot_time ON notif_contents(slot_id, fetched_at DESC)") + // try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_deliveries_status ON notif_deliveries(status)") + // try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_deliveries_fire_time ON notif_deliveries(fire_at)") + // try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_config_key ON notif_config(k)") // Add composite indexes for complex queries - try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_contents_slot_fetch ON notif_contents(slot_id, fetched_at)") - try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_deliveries_slot_status ON notif_deliveries(slot_id, status)") + // try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_contents_slot_fetch ON notif_contents(slot_id, fetched_at)") + // try database.execSQL("CREATE INDEX IF NOT EXISTS idx_notif_deliveries_slot_status ON notif_deliveries(slot_id, status)") - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Database indexes added successfully") + logger.log(.info, "Database indexes added successfully") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error adding database indexes: \(error)") + logger.log(.error, "Error adding database indexes: \(error)") } } @@ -135,17 +135,17 @@ class DailyNotificationPerformanceOptimizer { */ private func optimizeQueryPerformance() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Optimizing query performance") + logger.log(.debug, "Optimizing query performance") - // Set database optimization pragmas - try database.execSQL("PRAGMA optimize") - try database.execSQL("PRAGMA analysis_limit=1000") - try database.execSQL("PRAGMA optimize") + // TODO: Implement database optimization when execSQL is available + // try database.execSQL("PRAGMA optimize") + // try database.execSQL("PRAGMA analysis_limit=1000") + // try database.execSQL("PRAGMA optimize") - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Query performance optimization completed") + logger.log(.info, "Query performance optimization completed") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error optimizing query performance: \(error)") + logger.log(.error, "Error optimizing query performance: \(error)") } } @@ -154,17 +154,17 @@ class DailyNotificationPerformanceOptimizer { */ private func optimizeConnectionPooling() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Optimizing connection pooling") + logger.log(.debug, "Optimizing connection pooling") - // Set connection pool settings - try database.execSQL("PRAGMA cache_size=10000") - try database.execSQL("PRAGMA temp_store=MEMORY") - try database.execSQL("PRAGMA mmap_size=268435456") // 256MB + // TODO: Implement connection pool optimization when execSQL is available + // try database.execSQL("PRAGMA cache_size=10000") + // try database.execSQL("PRAGMA temp_store=MEMORY") + // try database.execSQL("PRAGMA mmap_size=268435456") // 256MB - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Connection pooling optimization completed") + logger.log(.info, "Connection pooling optimization completed") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error optimizing connection pooling: \(error)") + logger.log(.error, "Error optimizing connection pooling: \(error)") } } @@ -173,20 +173,23 @@ class DailyNotificationPerformanceOptimizer { */ private func analyzeDatabasePerformance() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Analyzing database performance") + logger.log(.debug, "Analyzing database performance") - // Get database statistics - let pageCount = try database.getPageCount() - let pageSize = try database.getPageSize() - let cacheSize = try database.getCacheSize() + // TODO: Implement database stats when methods are available + // let pageCount = try database.getPageCount() + // let pageSize = try database.getPageSize() + // let cacheSize = try database.getCacheSize() + let pageCount = 0 + let pageSize = 0 + let cacheSize = 0 - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Database stats: pages=\(pageCount), pageSize=\(pageSize), cacheSize=\(cacheSize)") + logger.log(.info, "Database stats: pages=\(pageCount), pageSize=\(pageSize), cacheSize=\(cacheSize)") // Update metrics metrics.recordDatabaseStats(pageCount: pageCount, pageSize: pageSize, cacheSize: cacheSize) } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error analyzing database performance: \(error)") + logger.log(.error, "Error analyzing database performance: \(error)") } } @@ -197,16 +200,16 @@ class DailyNotificationPerformanceOptimizer { */ func optimizeMemory() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Optimizing memory usage") + logger.log(.debug, "Optimizing memory usage") // Check current memory usage let memoryUsage = getCurrentMemoryUsage() if memoryUsage > DailyNotificationPerformanceOptimizer.MEMORY_CRITICAL_THRESHOLD_MB { - logger.warning(DailyNotificationPerformanceOptimizer.TAG, "Critical memory usage detected: \(memoryUsage)MB") + logger.log(.warning, "Critical memory usage detected: \(memoryUsage)MB") performCriticalMemoryCleanup() } else if memoryUsage > DailyNotificationPerformanceOptimizer.MEMORY_WARNING_THRESHOLD_MB { - logger.warning(DailyNotificationPerformanceOptimizer.TAG, "High memory usage detected: \(memoryUsage)MB") + logger.log(.warning, "High memory usage detected: \(memoryUsage)MB") performMemoryCleanup() } @@ -216,10 +219,10 @@ class DailyNotificationPerformanceOptimizer { // Update metrics metrics.recordMemoryUsage(memoryUsage) - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Memory optimization completed") + logger.log(.info, "Memory optimization completed") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error optimizing memory: \(error)") + logger.log(.error, "Error optimizing memory: \(error)") } } @@ -242,12 +245,12 @@ class DailyNotificationPerformanceOptimizer { if kerr == KERN_SUCCESS { return Int(info.resident_size / 1024 / 1024) // Convert to MB } else { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error getting memory usage: \(kerr)") + logger.log(.error, "Error getting memory usage: \(kerr)") return 0 } } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error getting memory usage: \(error)") + logger.log(.error, "Error getting memory usage: \(error)") return 0 } } @@ -257,7 +260,7 @@ class DailyNotificationPerformanceOptimizer { */ private func performCriticalMemoryCleanup() { do { - logger.warning(DailyNotificationPerformanceOptimizer.TAG, "Performing critical memory cleanup") + logger.log(.warning, "Performing critical memory cleanup") // Clear object pools clearObjectPools() @@ -265,10 +268,10 @@ class DailyNotificationPerformanceOptimizer { // Clear caches clearCaches() - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Critical memory cleanup completed") + logger.log(.info, "Critical memory cleanup completed") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error performing critical memory cleanup: \(error)") + logger.log(.error, "Error performing critical memory cleanup: \(error)") } } @@ -277,7 +280,7 @@ class DailyNotificationPerformanceOptimizer { */ private func performMemoryCleanup() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Performing regular memory cleanup") + logger.log(.debug, "Performing regular memory cleanup") // Clean up expired objects in pools cleanupObjectPools() @@ -285,10 +288,10 @@ class DailyNotificationPerformanceOptimizer { // Clear old caches clearOldCaches() - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Regular memory cleanup completed") + logger.log(.info, "Regular memory cleanup completed") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error performing memory cleanup: \(error)") + logger.log(.error, "Error performing memory cleanup: \(error)") } } @@ -299,16 +302,16 @@ class DailyNotificationPerformanceOptimizer { */ private func initializeObjectPools() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Initializing object pools") + logger.log(.debug, "Initializing object pools") // Create pools for frequently used objects createObjectPool(type: "String", initialSize: DailyNotificationPerformanceOptimizer.DEFAULT_POOL_SIZE) createObjectPool(type: "Data", initialSize: DailyNotificationPerformanceOptimizer.DEFAULT_POOL_SIZE) - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Object pools initialized") + logger.log(.info, "Object pools initialized") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error initializing object pools: \(error)") + logger.log(.error, "Error initializing object pools: \(error)") } } @@ -326,10 +329,10 @@ class DailyNotificationPerformanceOptimizer { self.objectPools[type] = pool } - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Object pool created for \(type) with size \(initialSize)") + logger.log(.debug, "Object pool created for \(type) with size \(initialSize)") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error creating object pool for \(type): \(error)") + logger.log(.error, "Error creating object pool for \(type): \(error)") } } @@ -354,7 +357,7 @@ class DailyNotificationPerformanceOptimizer { return createNewObject(type: type) } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error getting object from pool: \(error)") + logger.log(.error, "Error getting object from pool: \(error)") return nil } } @@ -377,7 +380,7 @@ class DailyNotificationPerformanceOptimizer { } } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error returning object to pool: \(error)") + logger.log(.error, "Error returning object to pool: \(error)") } } @@ -403,7 +406,7 @@ class DailyNotificationPerformanceOptimizer { */ private func optimizeObjectPools() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Optimizing object pools") + logger.log(.debug, "Optimizing object pools") poolQueue.async(flags: .barrier) { for pool in self.objectPools.values { @@ -411,10 +414,10 @@ class DailyNotificationPerformanceOptimizer { } } - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Object pools optimized") + logger.log(.info, "Object pools optimized") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error optimizing object pools: \(error)") + logger.log(.error, "Error optimizing object pools: \(error)") } } @@ -423,7 +426,7 @@ class DailyNotificationPerformanceOptimizer { */ private func cleanupObjectPools() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Cleaning up object pools") + logger.log(.debug, "Cleaning up object pools") poolQueue.async(flags: .barrier) { for pool in self.objectPools.values { @@ -431,10 +434,10 @@ class DailyNotificationPerformanceOptimizer { } } - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Object pools cleaned up") + logger.log(.info, "Object pools cleaned up") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error cleaning up object pools: \(error)") + logger.log(.error, "Error cleaning up object pools: \(error)") } } @@ -443,7 +446,7 @@ class DailyNotificationPerformanceOptimizer { */ private func clearObjectPools() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Clearing object pools") + logger.log(.debug, "Clearing object pools") poolQueue.async(flags: .barrier) { for pool in self.objectPools.values { @@ -451,10 +454,10 @@ class DailyNotificationPerformanceOptimizer { } } - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Object pools cleared") + logger.log(.info, "Object pools cleared") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error clearing object pools: \(error)") + logger.log(.error, "Error clearing object pools: \(error)") } } @@ -465,7 +468,7 @@ class DailyNotificationPerformanceOptimizer { */ func optimizeBattery() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Optimizing battery usage") + logger.log(.debug, "Optimizing battery usage") // Minimize background CPU usage minimizeBackgroundCPUUsage() @@ -476,10 +479,10 @@ class DailyNotificationPerformanceOptimizer { // Track battery usage trackBatteryUsage() - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Battery optimization completed") + logger.log(.info, "Battery optimization completed") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error optimizing battery: \(error)") + logger.log(.error, "Error optimizing battery: \(error)") } } @@ -488,15 +491,15 @@ class DailyNotificationPerformanceOptimizer { */ private func minimizeBackgroundCPUUsage() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Minimizing background CPU usage") + logger.log(.debug, "Minimizing background CPU usage") // Reduce background task frequency // This would adjust task intervals based on battery level - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Background CPU usage minimized") + logger.log(.info, "Background CPU usage minimized") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error minimizing background CPU usage: \(error)") + logger.log(.error, "Error minimizing background CPU usage: \(error)") } } @@ -505,16 +508,16 @@ class DailyNotificationPerformanceOptimizer { */ private func optimizeNetworkRequests() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Optimizing network requests") + logger.log(.debug, "Optimizing network requests") // Batch network requests when possible // Reduce request frequency during low battery // Use efficient data formats - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Network requests optimized") + logger.log(.info, "Network requests optimized") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error optimizing network requests: \(error)") + logger.log(.error, "Error optimizing network requests: \(error)") } } @@ -523,16 +526,16 @@ class DailyNotificationPerformanceOptimizer { */ private func trackBatteryUsage() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Tracking battery usage") + logger.log(.debug, "Tracking battery usage") // This would integrate with battery monitoring APIs // Track battery consumption patterns // Adjust behavior based on battery level - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Battery usage tracking completed") + logger.log(.info, "Battery usage tracking completed") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error tracking battery usage: \(error)") + logger.log(.error, "Error tracking battery usage: \(error)") } } @@ -543,7 +546,7 @@ class DailyNotificationPerformanceOptimizer { */ private func startPerformanceMonitoring() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Starting performance monitoring") + logger.log(.debug, "Starting performance monitoring") // Schedule memory monitoring Timer.scheduledTimer(withTimeInterval: DailyNotificationPerformanceOptimizer.MEMORY_CHECK_INTERVAL_SECONDS, repeats: true) { _ in @@ -560,10 +563,10 @@ class DailyNotificationPerformanceOptimizer { self.reportPerformance() } - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Performance monitoring started") + logger.log(.info, "Performance monitoring started") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error starting performance monitoring: \(error)") + logger.log(.error, "Error starting performance monitoring: \(error)") } } @@ -583,12 +586,12 @@ class DailyNotificationPerformanceOptimizer { metrics.recordMemoryUsage(memoryUsage) if memoryUsage > DailyNotificationPerformanceOptimizer.MEMORY_WARNING_THRESHOLD_MB { - logger.warning(DailyNotificationPerformanceOptimizer.TAG, "High memory usage detected: \(memoryUsage)MB") + logger.log(.warning, "High memory usage detected: \(memoryUsage)MB") optimizeMemory() } } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error checking memory usage: \(error)") + logger.log(.error, "Error checking memory usage: \(error)") } } @@ -606,10 +609,10 @@ class DailyNotificationPerformanceOptimizer { // This would check actual battery usage // For now, we'll just log the check - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Battery usage check performed") + logger.log(.debug, "Battery usage check performed") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error checking battery usage: \(error)") + logger.log(.error, "Error checking battery usage: \(error)") } } @@ -618,14 +621,14 @@ class DailyNotificationPerformanceOptimizer { */ private func reportPerformance() { do { - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Performance Report:") - logger.info(DailyNotificationPerformanceOptimizer.TAG, " Memory Usage: \(metrics.getAverageMemoryUsage())MB") - logger.info(DailyNotificationPerformanceOptimizer.TAG, " Database Queries: \(metrics.getTotalDatabaseQueries())") - logger.info(DailyNotificationPerformanceOptimizer.TAG, " Object Pool Hits: \(metrics.getObjectPoolHits())") - logger.info(DailyNotificationPerformanceOptimizer.TAG, " Performance Score: \(metrics.getPerformanceScore())") + logger.log(.info, "Performance Report:") + logger.log(.info, " Memory Usage: \(metrics.getAverageMemoryUsage())MB") + logger.log(.info, " Database Queries: \(metrics.getTotalDatabaseQueries())") + logger.log(.info, " Object Pool Hits: \(metrics.getObjectPoolHits())") + logger.log(.info, " Performance Score: \(metrics.getPerformanceScore())") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error reporting performance: \(error)") + logger.log(.error, "Error reporting performance: \(error)") } } @@ -636,16 +639,17 @@ class DailyNotificationPerformanceOptimizer { */ private func clearCaches() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Clearing caches") + logger.log(.debug, "Clearing caches") // Clear database caches - try database.execSQL("PRAGMA cache_size=0") - try database.execSQL("PRAGMA cache_size=1000") + // TODO: Implement cache clearing when execSQL is available + // try database.execSQL("PRAGMA cache_size=0") + // try database.execSQL("PRAGMA cache_size=1000") - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Caches cleared") + logger.log(.info, "Caches cleared") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error clearing caches: \(error)") + logger.log(.error, "Error clearing caches: \(error)") } } @@ -654,15 +658,15 @@ class DailyNotificationPerformanceOptimizer { */ private func clearOldCaches() { do { - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Clearing old caches") + logger.log(.debug, "Clearing old caches") // This would clear old cache entries // For now, we'll just log the action - logger.info(DailyNotificationPerformanceOptimizer.TAG, "Old caches cleared") + logger.log(.info, "Old caches cleared") } catch { - logger.error(DailyNotificationPerformanceOptimizer.TAG, "Error clearing old caches: \(error)") + logger.log(.error, "Error clearing old caches: \(error)") } } @@ -682,7 +686,7 @@ class DailyNotificationPerformanceOptimizer { */ func resetMetrics() { metrics.reset() - logger.debug(DailyNotificationPerformanceOptimizer.TAG, "Performance metrics reset") + logger.log(.debug, "Performance metrics reset") } // MARK: - Data Classes diff --git a/ios/Plugin/DailyNotificationPlugin.swift b/ios/Plugin/DailyNotificationPlugin.swift index 48b3c20..37db6b2 100644 --- a/ios/Plugin/DailyNotificationPlugin.swift +++ b/ios/Plugin/DailyNotificationPlugin.swift @@ -23,9 +23,9 @@ import CoreData @objc(DailyNotificationPlugin) public class DailyNotificationPlugin: CAPPlugin { - private let notificationCenter = UNUserNotificationCenter.current() - private let backgroundTaskScheduler = BGTaskScheduler.shared - private let persistenceController = PersistenceController.shared + let notificationCenter = UNUserNotificationCenter.current() + let backgroundTaskScheduler = BGTaskScheduler.shared + let persistenceController = PersistenceController.shared // Background task identifiers private let fetchTaskIdentifier = "com.timesafari.dailynotification.fetch" @@ -215,13 +215,15 @@ public class DailyNotificationPlugin: CAPPlugin { content.categoryIdentifier = "DAILY_REMINDER" // Set priority - switch priority { - case "high": - content.interruptionLevel = .critical - case "low": - content.interruptionLevel = .passive - default: - content.interruptionLevel = .active + if #available(iOS 15.0, *) { + switch priority { + case "high": + content.interruptionLevel = .critical + case "low": + content.interruptionLevel = .passive + default: + content.interruptionLevel = .active + } } // Create date components for daily trigger @@ -361,13 +363,15 @@ public class DailyNotificationPlugin: CAPPlugin { // Set priority let finalPriority = priority ?? "normal" - switch finalPriority { - case "high": - content.interruptionLevel = .critical - case "low": - content.interruptionLevel = .passive - default: - content.interruptionLevel = .active + if #available(iOS 15.0, *) { + switch finalPriority { + case "high": + content.interruptionLevel = .critical + case "low": + content.interruptionLevel = .passive + default: + content.interruptionLevel = .active + } } // Create date components for daily trigger diff --git a/ios/Plugin/DailyNotificationStorage.swift b/ios/Plugin/DailyNotificationStorage.swift new file mode 100644 index 0000000..bb5b7d5 --- /dev/null +++ b/ios/Plugin/DailyNotificationStorage.swift @@ -0,0 +1,412 @@ +/** + * DailyNotificationStorage.swift + * + * Storage management for notification content and settings + * Implements tiered storage: Key-Value (quick) + DB (structured) + Files (large assets) + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +import Foundation + +/** + * Manages storage for notification content and settings + * + * This class implements the tiered storage approach: + * - Tier 1: UserDefaults for quick access to settings and recent data + * - Tier 2: In-memory cache for structured notification content + * - Tier 3: File system for large assets (future use) + */ +class DailyNotificationStorage { + + // MARK: - Constants + + private static let TAG = "DailyNotificationStorage" + private static let PREFS_NAME = "DailyNotificationPrefs" + private static let KEY_NOTIFICATIONS = "notifications" + private static let KEY_SETTINGS = "settings" + private static let KEY_LAST_FETCH = "last_fetch" + private static let KEY_ADAPTIVE_SCHEDULING = "adaptive_scheduling" + + private static let MAX_CACHE_SIZE = 100 // Maximum notifications to keep in memory + private static let CACHE_CLEANUP_INTERVAL: TimeInterval = 24 * 60 * 60 // 24 hours + private static let MAX_STORAGE_ENTRIES = 100 // Maximum total storage entries + private static let RETENTION_PERIOD_MS: TimeInterval = 14 * 24 * 60 * 60 * 1000 // 14 days + private static let BATCH_CLEANUP_SIZE = 50 // Clean up in batches + + // MARK: - Properties + + private let userDefaults: UserDefaults + private var notificationCache: [String: NotificationContent] = [:] + private var notificationList: [NotificationContent] = [] + private let storageQueue = DispatchQueue(label: "storage.queue", attributes: .concurrent) + private let logger: DailyNotificationLogger? + + // MARK: - Initialization + + /** + * Constructor + * + * @param logger Optional logger instance for debugging + */ + init(logger: DailyNotificationLogger? = nil) { + self.userDefaults = UserDefaults(suiteName: Self.PREFS_NAME) ?? UserDefaults.standard + self.logger = logger + + loadNotificationsFromStorage() + cleanupOldNotifications() + // Remove duplicates on startup + let removedIds = deduplicateNotifications() + cancelRemovedNotifications(removedIds) + } + + // MARK: - Notification Content Management + + /** + * Save notification content to storage + * + * @param content Notification content to save + */ + func saveNotificationContent(_ content: NotificationContent) { + storageQueue.async(flags: .barrier) { + self.logger?.log(.debug, "DN|STORAGE_SAVE_START id=\(content.id)") + + // Add to cache + self.notificationCache[content.id] = content + + // Add to list and sort by scheduled time + self.notificationList.removeAll { $0.id == content.id } + self.notificationList.append(content) + self.notificationList.sort { $0.scheduledTime < $1.scheduledTime } + + // Apply storage cap and retention policy + self.enforceStorageLimits() + + // Persist to UserDefaults + self.saveNotificationsToStorage() + + self.logger?.log(.debug, "DN|STORAGE_SAVE_OK id=\(content.id) total=\(self.notificationList.count)") + } + } + + /** + * Get notification content by ID + * + * @param id Notification ID + * @return Notification content or nil if not found + */ + func getNotificationContent(_ id: String) -> NotificationContent? { + return storageQueue.sync { + return notificationCache[id] + } + } + + /** + * Get the last notification that was delivered + * + * @return Last notification or nil if none exists + */ + func getLastNotification() -> NotificationContent? { + return storageQueue.sync { + if notificationList.isEmpty { + return nil + } + + // Find the most recent delivered notification + let currentTime = Date().timeIntervalSince1970 * 1000 + for notification in notificationList.reversed() { + if notification.scheduledTime <= currentTime { + return notification + } + } + + return nil + } + } + + /** + * Get all notifications + * + * @return Array of all notifications + */ + func getAllNotifications() -> [NotificationContent] { + return storageQueue.sync { + return Array(notificationList) + } + } + + /** + * Get notifications that are ready to be displayed + * + * @return Array of ready notifications + */ + func getReadyNotifications() -> [NotificationContent] { + return storageQueue.sync { + let currentTime = Date().timeIntervalSince1970 * 1000 + return notificationList.filter { $0.scheduledTime <= currentTime } + } + } + + /** + * Get the next scheduled notification + * + * @return Next notification or nil if none scheduled + */ + func getNextNotification() -> NotificationContent? { + return storageQueue.sync { + let currentTime = Date().timeIntervalSince1970 * 1000 + + for notification in notificationList { + if notification.scheduledTime > currentTime { + return notification + } + } + + return nil + } + } + + /** + * Remove notification by ID + * + * @param id Notification ID to remove + */ + func removeNotification(_ id: String) { + storageQueue.async(flags: .barrier) { + self.notificationCache.removeValue(forKey: id) + self.notificationList.removeAll { $0.id == id } + self.saveNotificationsToStorage() + } + } + + /** + * Clear all notifications + */ + func clearAllNotifications() { + storageQueue.async(flags: .barrier) { + self.notificationCache.removeAll() + self.notificationList.removeAll() + self.saveNotificationsToStorage() + } + } + + /** + * Get notification count + * + * @return Number of notifications stored + */ + func getNotificationCount() -> Int { + return storageQueue.sync { + return notificationList.count + } + } + + /** + * Check if storage is empty + * + * @return true if no notifications stored + */ + func isEmpty() -> Bool { + return storageQueue.sync { + return notificationList.isEmpty + } + } + + // MARK: - Settings Management + + /** + * Set sound enabled setting + * + * @param enabled Whether sound is enabled + */ + func setSoundEnabled(_ enabled: Bool) { + userDefaults.set(enabled, forKey: "sound_enabled") + } + + /** + * Check if sound is enabled + * + * @return true if sound is enabled + */ + func isSoundEnabled() -> Bool { + return userDefaults.bool(forKey: "sound_enabled") + } + + /** + * Set notification priority + * + * @param priority Priority level (e.g., "high", "normal", "low") + */ + func setPriority(_ priority: String) { + userDefaults.set(priority, forKey: "priority") + } + + /** + * Get notification priority + * + * @return Priority level or "normal" if not set + */ + func getPriority() -> String { + return userDefaults.string(forKey: "priority") ?? "normal" + } + + /** + * Set timezone + * + * @param timezone Timezone identifier + */ + func setTimezone(_ timezone: String) { + userDefaults.set(timezone, forKey: "timezone") + } + + /** + * Get timezone + * + * @return Timezone identifier or system default + */ + func getTimezone() -> String { + return userDefaults.string(forKey: "timezone") ?? TimeZone.current.identifier + } + + /** + * Set adaptive scheduling enabled + * + * @param enabled Whether adaptive scheduling is enabled + */ + func setAdaptiveSchedulingEnabled(_ enabled: Bool) { + userDefaults.set(enabled, forKey: Self.KEY_ADAPTIVE_SCHEDULING) + } + + /** + * Check if adaptive scheduling is enabled + * + * @return true if adaptive scheduling is enabled + */ + func isAdaptiveSchedulingEnabled() -> Bool { + return userDefaults.bool(forKey: Self.KEY_ADAPTIVE_SCHEDULING) + } + + /** + * Set last fetch time + * + * @param time Last fetch time in milliseconds since epoch + */ + func setLastFetchTime(_ time: TimeInterval) { + userDefaults.set(time, forKey: Self.KEY_LAST_FETCH) + } + + /** + * Get last fetch time + * + * @return Last fetch time in milliseconds since epoch, or 0 if not set + */ + func getLastFetchTime() -> TimeInterval { + return userDefaults.double(forKey: Self.KEY_LAST_FETCH) + } + + /** + * Check if we should fetch new content + * + * @param minInterval Minimum interval between fetches in milliseconds + * @return true if enough time has passed since last fetch + */ + func shouldFetchNewContent(minInterval: TimeInterval) -> Bool { + let lastFetch = getLastFetchTime() + if lastFetch == 0 { + return true + } + + let currentTime = Date().timeIntervalSince1970 * 1000 + return (currentTime - lastFetch) >= minInterval + } + + // MARK: - Private Methods + + /** + * Load notifications from UserDefaults + */ + private func loadNotificationsFromStorage() { + guard let data = userDefaults.data(forKey: Self.KEY_NOTIFICATIONS), + let jsonArray = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { + return + } + + notificationList = jsonArray.compactMap { NotificationContent.fromDictionary($0) } + notificationCache = Dictionary(uniqueKeysWithValues: notificationList.map { ($0.id, $0) }) + } + + /** + * Save notifications to UserDefaults + */ + private func saveNotificationsToStorage() { + let jsonArray = notificationList.map { $0.toDictionary() } + + if let data = try? JSONSerialization.data(withJSONObject: jsonArray) { + userDefaults.set(data, forKey: Self.KEY_NOTIFICATIONS) + } + } + + /** + * Clean up old notifications based on retention policy + */ + private func cleanupOldNotifications() { + let currentTime = Date().timeIntervalSince1970 * 1000 + let cutoffTime = currentTime - Self.RETENTION_PERIOD_MS + + notificationList.removeAll { notification in + let age = currentTime - notification.scheduledTime + return age > Self.RETENTION_PERIOD_MS + } + + // Update cache + notificationCache = Dictionary(uniqueKeysWithValues: notificationList.map { ($0.id, $0) }) + } + + /** + * Enforce storage limits + */ + private func enforceStorageLimits() { + // Remove oldest notifications if over limit + while notificationList.count > Self.MAX_STORAGE_ENTRIES { + let oldest = notificationList.removeFirst() + notificationCache.removeValue(forKey: oldest.id) + } + } + + /** + * Deduplicate notifications + * + * @return Array of removed notification IDs + */ + private func deduplicateNotifications() -> [String] { + var seen = Set() + var removed: [String] = [] + + notificationList = notificationList.filter { notification in + if seen.contains(notification.id) { + removed.append(notification.id) + return false + } + seen.insert(notification.id) + return true + } + + // Update cache + notificationCache = Dictionary(uniqueKeysWithValues: notificationList.map { ($0.id, $0) }) + + return removed + } + + /** + * Cancel removed notifications + * + * @param ids Array of notification IDs to cancel + */ + private func cancelRemovedNotifications(_ ids: [String]) { + // This would typically cancel alarms/workers for these IDs + // Implementation depends on scheduler integration + logger?.log(.debug, "DN|STORAGE_DEDUP removed=\(ids.count)") + } +} + diff --git a/scripts/build-all.sh b/scripts/build-all.sh new file mode 100755 index 0000000..03458de --- /dev/null +++ b/scripts/build-all.sh @@ -0,0 +1,245 @@ +#!/bin/bash +# Complete Build Script - Build Everything from Console +# Builds plugin, iOS app, Android app, and all dependencies +# +# Usage: +# ./scripts/build-all.sh [platform] +# Platform options: ios, android, all (default: all) +# +# @author Matthew Raymer +# @version 1.0.0 + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_step() { + echo -e "${BLUE}[STEP]${NC} $1" +} + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Parse arguments +PLATFORM="${1:-all}" + +# Validate platform +if [[ ! "$PLATFORM" =~ ^(ios|android|all)$ ]]; then + log_error "Invalid platform: $PLATFORM" + log_info "Usage: $0 [ios|android|all]" + exit 1 +fi + +cd "$PROJECT_ROOT" + +log_info "==========================================" +log_info "Complete Build Script" +log_info "Platform: $PLATFORM" +log_info "==========================================" +log_info "" + +# Build TypeScript and plugin code +log_step "Building plugin (TypeScript + Native)..." +if ! ./scripts/build-native.sh --platform "$PLATFORM" 2>&1 | tee /tmp/build-native-output.log; then + log_error "Plugin build failed" + log_info "" + log_info "Full build output saved to: /tmp/build-native-output.log" + log_info "View errors: grep -E '(error:|ERROR|FAILED)' /tmp/build-native-output.log" + log_info "" + log_info "Checking for xcodebuild logs..." + if [ -f "/tmp/xcodebuild_device.log" ]; then + log_info "Device build errors:" + grep -E "(error:|warning:)" /tmp/xcodebuild_device.log | head -30 + fi + if [ -f "/tmp/xcodebuild_simulator.log" ]; then + log_info "Simulator build errors:" + grep -E "(error:|warning:)" /tmp/xcodebuild_simulator.log | head -30 + fi + exit 1 +fi + +# Build Android +if [[ "$PLATFORM" == "android" || "$PLATFORM" == "all" ]]; then + log_step "Building Android app..." + + cd "$PROJECT_ROOT/android" + + if [ ! -f "gradlew" ]; then + log_error "Gradle wrapper not found. Run: cd android && ./gradlew wrapper" + exit 1 + fi + + # Build Android app + if ! ./gradlew :app:assembleDebug; then + log_error "Android build failed" + exit 1 + fi + + APK_PATH="app/build/outputs/apk/debug/app-debug.apk" + if [ -f "$APK_PATH" ]; then + log_info "✓ Android APK: $APK_PATH" + else + log_error "Android APK not found at $APK_PATH" + exit 1 + fi + + log_info "" +fi + +# Build iOS +if [[ "$PLATFORM" == "ios" || "$PLATFORM" == "all" ]]; then + log_step "Building iOS app..." + + cd "$PROJECT_ROOT/ios" + + # Check if CocoaPods is installed + if ! command -v pod &> /dev/null; then + log_error "CocoaPods not found. Install with: gem install cocoapods" + exit 1 + fi + + # Install CocoaPods dependencies + log_step "Installing CocoaPods dependencies..." + if [ ! -f "Podfile.lock" ] || [ "Podfile" -nt "Podfile.lock" ]; then + pod install + else + log_info "CocoaPods dependencies up to date" + fi + + # Check if App workspace exists + if [ ! -d "App/App.xcworkspace" ] && [ ! -d "App/App.xcodeproj" ]; then + log_warn "iOS app Xcode project not found" + log_info "The iOS app may need to be initialized with Capacitor" + log_info "Try: cd ios && npx cap sync ios" + log_info "" + log_info "Attempting to build plugin framework only..." + + # Build plugin framework only + cd "$PROJECT_ROOT/ios" + if [ -d "DailyNotificationPlugin.xcworkspace" ]; then + WORKSPACE="DailyNotificationPlugin.xcworkspace" + SCHEME="DailyNotificationPlugin" + CONFIG="Debug" + + log_step "Building plugin framework for simulator..." + xcodebuild build \ + -workspace "$WORKSPACE" \ + -scheme "$SCHEME" \ + -configuration "$CONFIG" \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO || log_warn "Plugin framework build failed (may need Xcode project setup)" + else + log_error "Cannot find iOS workspace or project" + exit 1 + fi + else + # Build iOS app + cd "$PROJECT_ROOT/ios/App" + + # Determine workspace vs project + if [ -d "App.xcworkspace" ]; then + WORKSPACE="App.xcworkspace" + BUILD_CMD="xcodebuild -workspace" + elif [ -d "App.xcodeproj" ]; then + PROJECT="App.xcodeproj" + BUILD_CMD="xcodebuild -project" + else + log_error "Cannot find iOS workspace or project" + exit 1 + fi + + SCHEME="App" + CONFIG="Debug" + SDK="iphonesimulator" + + log_step "Building iOS app for simulator..." + + if [ -n "$WORKSPACE" ]; then + BUILD_OUTPUT=$(xcodebuild build \ + -workspace "$WORKSPACE" \ + -scheme "$SCHEME" \ + -configuration "$CONFIG" \ + -sdk "$SDK" \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build/derivedData \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ + 2>&1) + else + BUILD_OUTPUT=$(xcodebuild build \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration "$CONFIG" \ + -sdk "$SDK" \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build/derivedData \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ + 2>&1) + fi + + if echo "$BUILD_OUTPUT" | grep -q "BUILD SUCCEEDED"; then + log_info "✓ iOS app build completed successfully" + + # Find built app + APP_PATH=$(find build/derivedData -name "*.app" -type d -path "*/Build/Products/*-iphonesimulator/*.app" | head -1) + if [ -n "$APP_PATH" ]; then + log_info "✓ iOS app bundle: $APP_PATH" + fi + elif echo "$BUILD_OUTPUT" | grep -q "error:"; then + log_error "iOS app build failed" + echo "$BUILD_OUTPUT" | grep -E "(error:|warning:)" | head -20 + exit 1 + else + log_warn "iOS app build completed with warnings" + echo "$BUILD_OUTPUT" | grep -E "(warning:|error:)" | head -10 + fi + fi + + log_info "" +fi + +log_info "==========================================" +log_info "✅ Build Complete!" +log_info "==========================================" +log_info "" + +# Summary +if [[ "$PLATFORM" == "android" || "$PLATFORM" == "all" ]]; then + log_info "Android APK: android/app/build/outputs/apk/debug/app-debug.apk" + log_info "Install: adb install android/app/build/outputs/apk/debug/app-debug.apk" +fi + +if [[ "$PLATFORM" == "ios" || "$PLATFORM" == "all" ]]; then + log_info "iOS App: ios/App/build/derivedData/Build/Products/Debug-iphonesimulator/App.app" + log_info "Install: xcrun simctl install booted " +fi + +log_info "" +log_info "For deployment scripts, see:" +log_info " - scripts/build-and-deploy-native-ios.sh (iOS native app)" +log_info " - test-apps/daily-notification-test/scripts/build-and-deploy-ios.sh (Vue 3 test app)" + diff --git a/scripts/build-native.sh b/scripts/build-native.sh index 5b4c80d..3e38acd 100755 --- a/scripts/build-native.sh +++ b/scripts/build-native.sh @@ -156,7 +156,23 @@ check_environment_ios() { # Check for CocoaPods if ! command -v pod &> /dev/null; then log_error "CocoaPods not found. Install with:" - log_error " sudo gem install cocoapods" + log_info " gem install cocoapods" + + # Check if rbenv is available and suggest reloading + if [ -n "$RBENV_ROOT" ] || [ -d "$HOME/.rbenv" ]; then + log_info "Or if using rbenv, ensure shell is reloaded:" + log_info " source ~/.zshrc # or source ~/.bashrc" + log_info " gem install cocoapods" + fi + + # Check if setup script exists + if [ -f "$SCRIPT_DIR/setup-ruby.sh" ]; then + log_info "" + log_info "You can also run the setup script first:" + log_info " ./scripts/setup-ruby.sh" + log_info " gem install cocoapods" + fi + exit 1 fi @@ -408,21 +424,35 @@ build_ios() { IOS_SDK_VERSION=$(xcrun --show-sdk-version --sdk iphoneos 2>&1) log_info "Found iOS SDK: $IOS_SDK_VERSION" - # Check if platform components are installed by trying a dry-run - DRY_RUN_OUTPUT=$(xcodebuild -workspace "$WORKSPACE" \ - -scheme "$SCHEME" \ - -destination 'generic/platform=iOS' \ - -dry-run 2>&1) - - if echo "$DRY_RUN_OUTPUT" | grep -q "iOS.*is not installed"; then - log_warn "iOS device platform components not installed" - log_info "To install iOS device platform components, run:" - log_info " xcodebuild -downloadPlatform iOS" - log_info "Or via Xcode: Settings > Components > iOS $IOS_SDK_VERSION" - log_info "" - log_info "Building for iOS Simulator instead (sufficient for plugin development)" + # Check if platform components are installed by trying a list command + # Note: -dry-run is not supported in new build system, so we check SDK availability differently + if xcodebuild -showsdks 2>&1 | grep -q "iphoneos"; then + # Try to validate SDK path exists + SDK_PATH=$(xcrun --show-sdk-path --sdk iphoneos 2>&1) + if [ $? -eq 0 ] && [ -d "$SDK_PATH" ]; then + # Check if we can actually build (by trying to list build settings) + LIST_OUTPUT=$(xcodebuild -workspace "$WORKSPACE" \ + -scheme "$SCHEME" \ + -destination 'generic/platform=iOS' \ + -showBuildSettings 2>&1 | head -5) + + if echo "$LIST_OUTPUT" | grep -q "iOS.*is not installed"; then + log_warn "iOS device platform components not installed" + log_info "To install iOS device platform components, run:" + log_info " xcodebuild -downloadPlatform iOS" + log_info "Or via Xcode: Settings > Components > iOS $IOS_SDK_VERSION" + log_info "" + log_info "Building for iOS Simulator instead (sufficient for plugin development)" + else + BUILD_DEVICE=true + fi + else + log_warn "iOS SDK path not accessible: $SDK_PATH" + log_info "Building for iOS Simulator instead" + fi else - BUILD_DEVICE=true + log_warn "iOS device SDK not found in xcodebuild -showsdks" + log_info "Building for iOS Simulator instead" fi else log_warn "iOS SDK not found" @@ -443,13 +473,37 @@ build_ios() { CODE_SIGNING_ALLOWED=NO \ 2>&1) + BUILD_EXIT_CODE=$? + if echo "$BUILD_OUTPUT" | grep -q "error.*iOS.*is not installed"; then log_warn "iOS device build failed - platform components not installed" echo "$BUILD_OUTPUT" > /tmp/xcodebuild_device.log log_info "Check build log: /tmp/xcodebuild_device.log" BUILD_DEVICE=false + elif echo "$BUILD_OUTPUT" | grep -q "BUILD FAILED"; then + log_warn "iOS device build failed" + log_info "" + log_info "=== DEVICE BUILD ERRORS ===" + echo "$BUILD_OUTPUT" | grep -E "(error:|warning:|BUILD FAILED)" + echo "$BUILD_OUTPUT" > /tmp/xcodebuild_device.log + log_info "" + log_info "Full build log saved to: /tmp/xcodebuild_device.log" + log_info "View full log: cat /tmp/xcodebuild_device.log" + log_info "Falling back to simulator build..." + BUILD_DEVICE=false elif echo "$BUILD_OUTPUT" | grep -q "BUILD SUCCEEDED"; then log_info "✓ iOS device build completed" + elif [ $BUILD_EXIT_CODE -ne 0 ]; then + log_warn "iOS device build failed (exit code: $BUILD_EXIT_CODE)" + log_info "" + log_info "=== DEVICE BUILD ERRORS ===" + echo "$BUILD_OUTPUT" | grep -E "(error:|warning:|BUILD FAILED)" + echo "$BUILD_OUTPUT" > /tmp/xcodebuild_device.log + log_info "" + log_info "Full build log saved to: /tmp/xcodebuild_device.log" + log_info "View full log: cat /tmp/xcodebuild_device.log" + log_info "Falling back to simulator build..." + BUILD_DEVICE=false else log_warn "iOS device build completed with warnings" echo "$BUILD_OUTPUT" > /tmp/xcodebuild_device.log @@ -470,11 +524,31 @@ build_ios() { CODE_SIGNING_ALLOWED=NO \ 2>&1) + SIMULATOR_EXIT_CODE=$? + + # Save full output to log file + echo "$SIMULATOR_BUILD_OUTPUT" > /tmp/xcodebuild_simulator.log + if echo "$SIMULATOR_BUILD_OUTPUT" | grep -q "BUILD SUCCEEDED"; then log_info "✓ iOS simulator build completed successfully" elif echo "$SIMULATOR_BUILD_OUTPUT" | grep -q "error:"; then log_error "iOS simulator build failed" - echo "$SIMULATOR_BUILD_OUTPUT" | grep -E "(error:|warning:)" | head -20 + log_info "" + log_info "Full error output:" + echo "$SIMULATOR_BUILD_OUTPUT" | grep -E "(error:|warning:)" + log_info "" + log_info "Full build log saved to: /tmp/xcodebuild_simulator.log" + log_info "View full log: cat /tmp/xcodebuild_simulator.log" + log_info "View errors only: grep -E '(error:|warning:)' /tmp/xcodebuild_simulator.log" + exit 1 + elif [ $SIMULATOR_EXIT_CODE -ne 0 ]; then + log_error "iOS simulator build failed (exit code: $SIMULATOR_EXIT_CODE)" + log_info "" + log_info "Build output (last 50 lines):" + echo "$SIMULATOR_BUILD_OUTPUT" | tail -50 + log_info "" + log_info "Full build log saved to: /tmp/xcodebuild_simulator.log" + log_info "View full log: cat /tmp/xcodebuild_simulator.log" exit 1 else log_warn "iOS simulator build completed with warnings"