Browse Source

fix(ios): resolve compilation errors and enable successful build

Fixed critical compilation errors preventing iOS plugin build:
- Updated logger API calls from logger.debug(TAG, msg) to logger.log(.debug, msg)
  across all iOS plugin files to match DailyNotificationLogger interface
- Fixed async/await concurrency in makeConditionalRequest using semaphore pattern
- Fixed NotificationContent immutability by creating new instances instead of mutation
- Changed private access control to internal for extension-accessible methods
- Added iOS 15.0+ availability checks for interruptionLevel property
- Fixed static member references using Self.MEMBER_NAME syntax
- Added missing .scheduling case to exhaustive switch statement
- Fixed variable initialization in retry state closures

Added DailyNotificationStorage.swift implementation matching Android pattern.

Updated build scripts with improved error reporting and full log visibility.

iOS plugin now compiles successfully. All build errors resolved.
ios-implementation
Matthew Raymer 1 day ago
parent
commit
8ded555a21
  1. 291
      docs/CONSOLE_BUILD_GUIDE.md
  2. 199
      docs/VIEWING_BUILD_ERRORS.md
  3. 185
      docs/ios-native-interface.md
  4. 106
      ios/App/App/AppDelegate.swift
  5. 119
      ios/App/App/Info.plist
  6. 61
      ios/App/App/SceneDelegate.swift
  7. 69
      ios/App/App/ViewController.swift
  8. 28
      ios/Plugin/DailyNotificationBackgroundTaskManager.swift
  9. 6
      ios/Plugin/DailyNotificationBackgroundTasks.swift
  10. 5
      ios/Plugin/DailyNotificationCallbacks.swift
  11. 79
      ios/Plugin/DailyNotificationETagManager.swift
  12. 47
      ios/Plugin/DailyNotificationErrorHandler.swift
  13. 200
      ios/Plugin/DailyNotificationPerformanceOptimizer.swift
  14. 38
      ios/Plugin/DailyNotificationPlugin.swift
  15. 412
      ios/Plugin/DailyNotificationStorage.swift
  16. 245
      scripts/build-all.sh
  17. 106
      scripts/build-native.sh

291
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)

199
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
```

185
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
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
```
### iOS (Info.plist)
```xml
<key>NSUserNotificationsUsageDescription</key>
<string>This app uses notifications to deliver daily updates and reminders.</string>
<key>UIBackgroundModes</key>
<array>
<string>background-fetch</string>
<string>background-processing</string>
<string>remote-notification</string>
</array>
```
## 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)

106
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)")
}
}

119
ios/App/App/Info.plist

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- App Display Name -->
<key>CFBundleDisplayName</key>
<string>DailyNotification Test</string>
<!-- Bundle Identifier -->
<key>CFBundleIdentifier</key>
<string>com.timesafari.dailynotification</string>
<!-- Bundle Name -->
<key>CFBundleName</key>
<string>DailyNotification Test App</string>
<!-- Version -->
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<!-- Build Number -->
<key>CFBundleVersion</key>
<string>1</string>
<!-- Minimum iOS Version -->
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<!-- Device Family -->
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<!-- Supported Interface Orientations -->
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<!-- Supported Interface Orientations (iPad) -->
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<!-- Status Bar Style -->
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<!-- Status Bar Hidden -->
<key>UIStatusBarHidden</key>
<false/>
<!-- Launch Screen -->
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<!-- Privacy Usage Descriptions -->
<key>NSUserNotificationsUsageDescription</key>
<string>This app uses notifications to deliver daily updates and reminders.</string>
<!-- Background Modes -->
<key>UIBackgroundModes</key>
<array>
<string>background-fetch</string>
<string>background-processing</string>
<string>remote-notification</string>
</array>
<!-- Background Task Identifiers -->
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.timesafari.dailynotification.fetch</string>
<string>com.timesafari.dailynotification.notify</string>
</array>
<!-- App Transport Security -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<!-- Add your callback domains here -->
</dict>
</dict>
<!-- Scene Configuration (iOS 13+) -->
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<!-- Background App Refresh -->
<key>UIApplicationExitsOnSuspend</key>
<false/>
</dict>
</plist>

61
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
}
}

69
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
}
}

28
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)
}

6
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> = ContentCache.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \ContentCache.fetchedAt, ascending: false)]

5
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> = 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)

79
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)")
}
}

47
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

200
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

38
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

412
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<String>()
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)")
}
}

245
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 <APP_PATH>"
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)"

106
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"

Loading…
Cancel
Save