Browse Source
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
17 changed files with 2000 additions and 196 deletions
@ -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) |
|||
|
|||
@ -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 |
|||
``` |
|||
|
|||
@ -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) |
|||
|
|||
@ -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)") |
|||
} |
|||
} |
|||
|
|||
@ -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> |
|||
|
|||
@ -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 |
|||
} |
|||
} |
|||
|
|||
@ -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 |
|||
} |
|||
} |
|||
|
|||
@ -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)") |
|||
} |
|||
} |
|||
|
|||
@ -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)" |
|||
|
|||
Loading…
Reference in new issue