Compare commits
2 Commits
master
...
ios-implem
| Author | SHA1 | Date |
|---|---|---|
|
|
8ded555a21 | 1 day ago |
|
|
4be87acc14 | 2 days ago |
28 changed files with 4309 additions and 288 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,64 @@ |
|||
# Web Assets Structure - Android and iOS Parity |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Date**: November 4, 2025 |
|||
|
|||
## Source of Truth |
|||
|
|||
The **`www/`** directory is the source of truth for web assets. Both Android and iOS app directories should match this structure. |
|||
|
|||
## Directory Structure |
|||
|
|||
``` |
|||
www/ # Source of truth (edit here) |
|||
├── index.html # Main test interface |
|||
|
|||
android/app/src/main/assets/ # Android (synced from www/) |
|||
├── capacitor.plugins.json # Auto-generated by Capacitor |
|||
└── public/ # Web assets (must match www/) |
|||
└── index.html # Synced from www/index.html |
|||
|
|||
ios/App/App/ # iOS (synced from www/) |
|||
├── capacitor.config.json # Capacitor configuration |
|||
└── public/ # Web assets (must match www/) |
|||
└── index.html # Synced from www/index.html |
|||
``` |
|||
|
|||
## Synchronization |
|||
|
|||
Both `android/app/src/main/assets/public/` and `ios/App/App/public/` should match `www/` after running: |
|||
|
|||
```bash |
|||
# Sync web assets to both platforms |
|||
npx cap sync |
|||
|
|||
# Or sync individually |
|||
npx cap sync android |
|||
npx cap sync ios |
|||
``` |
|||
|
|||
## Key Points |
|||
|
|||
1. **Edit source files in `www/`** - Never edit platform-specific copies directly |
|||
2. **Both platforms should match** - After sync, `android/.../assets/public/` and `ios/App/App/public/` should be identical |
|||
3. **Capacitor handles sync** - `npx cap sync` copies files from `www/` to platform directories |
|||
4. **Auto-generated files** - `capacitor.plugins.json`, `capacitor.js`, etc. are generated by Capacitor |
|||
|
|||
## Verification |
|||
|
|||
After syncing, verify both platforms match: |
|||
|
|||
```bash |
|||
# Check file sizes match |
|||
ls -lh www/index.html android/app/src/main/assets/public/index.html ios/App/App/public/index.html |
|||
|
|||
# Compare contents |
|||
diff www/index.html android/app/src/main/assets/public/index.html |
|||
diff www/index.html ios/App/App/public/index.html |
|||
``` |
|||
|
|||
## Notes |
|||
|
|||
- **Cordova files**: iOS may have empty `cordova.js` and `cordova_plugins.js` files. These are harmless but should be removed if not using Cordova compatibility. |
|||
- **Capacitor runtime**: Capacitor generates `capacitor.js` and `capacitor_plugins.js` during sync - these are auto-generated and should not be manually edited. |
|||
|
|||
@ -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,548 @@ |
|||
# Running iOS Apps in Standalone Simulator (Without Xcode UI) |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Last Updated**: 2025-11-04 |
|||
**Version**: 1.0.0 |
|||
|
|||
## Overview |
|||
|
|||
This guide demonstrates how to run DailyNotification plugin test apps in a standalone iOS Simulator without using Xcode UI. This method is useful for development, CI/CD pipelines, and command-line workflows. |
|||
|
|||
**There are two different test apps:** |
|||
1. **Native iOS Development App** (`ios/App`) - Simple Capacitor app for plugin development |
|||
2. **Vue 3 Test App** (`test-apps/daily-notification-test`) - Full-featured Vue 3 Capacitor app for comprehensive testing |
|||
|
|||
## Prerequisites |
|||
|
|||
### Required Software |
|||
- **Xcode** with command line tools (`xcode-select --install`) |
|||
- **iOS Simulator** (included with Xcode) |
|||
- **CocoaPods** (`gem install cocoapods`) |
|||
- **Node.js** and **npm** (for TypeScript compilation) |
|||
- **Capacitor CLI** (`npm install -g @capacitor/cli`) - for Vue 3 test app |
|||
|
|||
### System Requirements |
|||
- **macOS** (required for iOS development) |
|||
- **RAM**: 4GB minimum, 8GB recommended |
|||
- **Storage**: 5GB free space for simulator and dependencies |
|||
|
|||
--- |
|||
|
|||
## Scenario 1: Native iOS Development App (`ios/App`) |
|||
|
|||
The `ios/App` directory contains a simple Capacitor-based development app, similar to `android/app`. This is used for quick plugin testing and development. |
|||
|
|||
### Step-by-Step Process |
|||
|
|||
#### 1. Check Available Simulators |
|||
|
|||
```bash |
|||
# List available iOS simulators |
|||
xcrun simctl list devices available |
|||
|
|||
# Example output: |
|||
# iPhone 15 Pro (iOS 17.0) |
|||
# iPhone 14 (iOS 16.4) |
|||
# iPad Pro (12.9-inch) (iOS 17.0) |
|||
``` |
|||
|
|||
#### 2. Boot a Simulator |
|||
|
|||
```bash |
|||
# Boot a specific simulator device |
|||
xcrun simctl boot "iPhone 15 Pro" |
|||
|
|||
# Or boot by device ID |
|||
xcrun simctl boot <DEVICE_ID> |
|||
|
|||
# Verify simulator is running |
|||
xcrun simctl list devices | grep Booted |
|||
``` |
|||
|
|||
**Alternative: Open Simulator UI** |
|||
```bash |
|||
# Open Simulator app (allows visual interaction) |
|||
open -a Simulator |
|||
``` |
|||
|
|||
#### 3. Build the Plugin |
|||
|
|||
```bash |
|||
# Navigate to project directory |
|||
cd /path/to/daily-notification-plugin |
|||
|
|||
# Build plugin for iOS |
|||
./scripts/build-native.sh --platform ios |
|||
``` |
|||
|
|||
**What this does:** |
|||
- Compiles TypeScript to JavaScript |
|||
- Builds iOS native code (Swift) |
|||
- Creates plugin framework |
|||
- Builds for simulator |
|||
|
|||
#### 4. Build Native iOS Development App |
|||
|
|||
```bash |
|||
# Navigate to iOS directory |
|||
cd ios |
|||
|
|||
# Install CocoaPods dependencies |
|||
pod install |
|||
|
|||
# Build the development app for simulator |
|||
cd App |
|||
xcodebuild -workspace App.xcworkspace \ |
|||
-scheme App \ |
|||
-configuration Debug \ |
|||
-sdk iphonesimulator \ |
|||
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
|||
-derivedDataPath build/derivedData \ |
|||
CODE_SIGN_IDENTITY="" \ |
|||
CODE_SIGNING_REQUIRED=NO \ |
|||
CODE_SIGNING_ALLOWED=NO \ |
|||
clean build |
|||
``` |
|||
|
|||
#### 5. Install App on Simulator |
|||
|
|||
```bash |
|||
# Find the built app |
|||
APP_PATH=$(find build/derivedData -name "*.app" -type d -path "*/Build/Products/*-iphonesimulator/*.app" | head -1) |
|||
|
|||
# Install app on simulator |
|||
xcrun simctl install booted "$APP_PATH" |
|||
``` |
|||
|
|||
#### 6. Launch the App |
|||
|
|||
```bash |
|||
# Get bundle identifier from Info.plist |
|||
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
|||
|
|||
# Launch the app |
|||
xcrun simctl launch booted "$BUNDLE_ID" |
|||
|
|||
# Example: |
|||
# xcrun simctl launch booted com.timesafari.dailynotification |
|||
``` |
|||
|
|||
#### 7. Monitor App Logs |
|||
|
|||
```bash |
|||
# View all logs |
|||
xcrun simctl spawn booted log stream |
|||
|
|||
# Filter for specific processes |
|||
xcrun simctl spawn booted log stream --predicate 'processImagePath contains "App"' |
|||
|
|||
# View system logs |
|||
log stream --predicate 'processImagePath contains "App"' --level debug |
|||
``` |
|||
|
|||
### Complete Command Sequence for Native iOS App |
|||
|
|||
```bash |
|||
# 1. Boot simulator |
|||
xcrun simctl boot "iPhone 15 Pro" || open -a Simulator |
|||
|
|||
# 2. Build plugin |
|||
cd /path/to/daily-notification-plugin |
|||
./scripts/build-native.sh --platform ios |
|||
|
|||
# 3. Build native iOS app |
|||
cd ios |
|||
pod install |
|||
cd App |
|||
xcodebuild -workspace App.xcworkspace \ |
|||
-scheme App \ |
|||
-configuration Debug \ |
|||
-sdk iphonesimulator \ |
|||
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
|||
CODE_SIGN_IDENTITY="" \ |
|||
CODE_SIGNING_REQUIRED=NO \ |
|||
CODE_SIGNING_ALLOWED=NO |
|||
|
|||
# 4. Install app |
|||
APP_PATH=$(find build/derivedData -name "*.app" -type d | head -1) |
|||
xcrun simctl install booted "$APP_PATH" |
|||
|
|||
# 5. Launch app |
|||
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
|||
xcrun simctl launch booted "$BUNDLE_ID" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Scenario 2: Vue 3 Test App (`test-apps/daily-notification-test`) |
|||
|
|||
The `test-apps/daily-notification-test` directory contains a full-featured Vue 3 Capacitor app with comprehensive testing interface, similar to the Android test app. |
|||
|
|||
### Step-by-Step Process |
|||
|
|||
#### 1. Check Available Simulators |
|||
|
|||
```bash |
|||
# List available iOS simulators |
|||
xcrun simctl list devices available |
|||
``` |
|||
|
|||
#### 2. Boot a Simulator |
|||
|
|||
```bash |
|||
# Boot a specific simulator device |
|||
xcrun simctl boot "iPhone 15 Pro" |
|||
|
|||
# Or open Simulator UI |
|||
open -a Simulator |
|||
``` |
|||
|
|||
#### 3. Build the Plugin |
|||
|
|||
```bash |
|||
# Navigate to project directory |
|||
cd /path/to/daily-notification-plugin |
|||
|
|||
# Build plugin for iOS |
|||
./scripts/build-native.sh --platform ios |
|||
``` |
|||
|
|||
#### 4. Set Up Vue 3 Test App iOS Project |
|||
|
|||
```bash |
|||
# Navigate to test app |
|||
cd test-apps/daily-notification-test |
|||
|
|||
# Install dependencies |
|||
npm install |
|||
|
|||
# Add iOS platform (if not already added) |
|||
npx cap add ios |
|||
|
|||
# Sync web assets with iOS project |
|||
npx cap sync ios |
|||
``` |
|||
|
|||
#### 5. Build Vue 3 Test App |
|||
|
|||
```bash |
|||
# Build web assets (Vue 3 app) |
|||
npm run build |
|||
|
|||
# Sync with iOS project |
|||
npx cap sync ios |
|||
|
|||
# Build iOS app for simulator |
|||
cd ios/App |
|||
xcodebuild -workspace App.xcworkspace \ |
|||
-scheme App \ |
|||
-configuration Debug \ |
|||
-sdk iphonesimulator \ |
|||
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
|||
-derivedDataPath build/derivedData \ |
|||
CODE_SIGN_IDENTITY="" \ |
|||
CODE_SIGNING_REQUIRED=NO \ |
|||
CODE_SIGNING_ALLOWED=NO |
|||
``` |
|||
|
|||
#### 6. Install App on Simulator |
|||
|
|||
```bash |
|||
# Find the built app |
|||
APP_PATH=$(find build/derivedData -name "*.app" -type d -path "*/Build/Products/*-iphonesimulator/*.app" | head -1) |
|||
|
|||
# Install app on simulator |
|||
xcrun simctl install booted "$APP_PATH" |
|||
``` |
|||
|
|||
#### 7. Launch the App |
|||
|
|||
```bash |
|||
# Get bundle identifier |
|||
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
|||
|
|||
# Launch the app |
|||
xcrun simctl launch booted "$BUNDLE_ID" |
|||
|
|||
# Example: |
|||
# xcrun simctl launch booted com.timesafari.dailynotification.test |
|||
``` |
|||
|
|||
### Complete Command Sequence for Vue 3 Test App |
|||
|
|||
```bash |
|||
# 1. Boot simulator |
|||
xcrun simctl boot "iPhone 15 Pro" || open -a Simulator |
|||
|
|||
# 2. Build plugin |
|||
cd /path/to/daily-notification-plugin |
|||
./scripts/build-native.sh --platform ios |
|||
|
|||
# 3. Set up Vue 3 test app |
|||
cd test-apps/daily-notification-test |
|||
npm install |
|||
npm run build |
|||
|
|||
# 4. Sync with iOS |
|||
npx cap sync ios |
|||
|
|||
# 5. Build iOS app |
|||
cd ios/App |
|||
xcodebuild -workspace App.xcworkspace \ |
|||
-scheme App \ |
|||
-configuration Debug \ |
|||
-sdk iphonesimulator \ |
|||
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
|||
CODE_SIGN_IDENTITY="" \ |
|||
CODE_SIGNING_REQUIRED=NO \ |
|||
CODE_SIGNING_ALLOWED=NO |
|||
|
|||
# 6. Install app |
|||
APP_PATH=$(find build/derivedData -name "*.app" -type d | head -1) |
|||
xcrun simctl install booted "$APP_PATH" |
|||
|
|||
# 7. Launch app |
|||
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
|||
xcrun simctl launch booted "$BUNDLE_ID" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Alternative Methods |
|||
|
|||
### Method 1: Using Capacitor CLI (Vue 3 Test App Only) |
|||
|
|||
```bash |
|||
cd test-apps/daily-notification-test |
|||
|
|||
# Build and run in one command |
|||
npx cap run ios |
|||
|
|||
# This will: |
|||
# - Build the plugin |
|||
# - Sync web assets |
|||
# - Build and install app |
|||
# - Launch in simulator |
|||
``` |
|||
|
|||
**Note**: This method only works for the Vue 3 test app, not the native iOS development app. |
|||
|
|||
### Method 2: Using Automated Scripts |
|||
|
|||
#### For Native iOS App (`ios/App`) |
|||
|
|||
Create `scripts/build-and-deploy-native-ios.sh`: |
|||
|
|||
```bash |
|||
#!/bin/bash |
|||
set -e |
|||
|
|||
echo "🔨 Building plugin..." |
|||
cd /path/to/daily-notification-plugin |
|||
./scripts/build-native.sh --platform ios |
|||
|
|||
echo "📱 Booting simulator..." |
|||
xcrun simctl boot "iPhone 15 Pro" 2>/dev/null || open -a Simulator |
|||
|
|||
echo "🏗️ Building native iOS app..." |
|||
cd ios |
|||
pod install |
|||
cd App |
|||
xcodebuild -workspace App.xcworkspace \ |
|||
-scheme App \ |
|||
-configuration Debug \ |
|||
-sdk iphonesimulator \ |
|||
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
|||
CODE_SIGN_IDENTITY="" \ |
|||
CODE_SIGNING_REQUIRED=NO \ |
|||
CODE_SIGNING_ALLOWED=NO |
|||
|
|||
APP_PATH=$(find build/derivedData -name "*.app" -type d | head -1) |
|||
xcrun simctl install booted "$APP_PATH" |
|||
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
|||
xcrun simctl launch booted "$BUNDLE_ID" |
|||
``` |
|||
|
|||
#### For Vue 3 Test App |
|||
|
|||
Use the existing script: |
|||
```bash |
|||
cd test-apps/daily-notification-test |
|||
./scripts/build-and-deploy-ios.sh |
|||
``` |
|||
|
|||
### Method 3: Manual Xcode Build |
|||
|
|||
```bash |
|||
# Open project in Xcode |
|||
open ios/App/App.xcworkspace # For native app |
|||
# OR |
|||
open test-apps/daily-notification-test/ios/App/App.xcworkspace # For Vue 3 app |
|||
|
|||
# Then: |
|||
# 1. Select simulator target |
|||
# 2. Click Run button (⌘R) |
|||
# 3. App builds and launches automatically |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Simulator Management |
|||
|
|||
### List Available Devices |
|||
|
|||
```bash |
|||
# List all devices |
|||
xcrun simctl list devices |
|||
|
|||
# List only available devices |
|||
xcrun simctl list devices available |
|||
|
|||
# List booted devices |
|||
xcrun simctl list devices | grep Booted |
|||
``` |
|||
|
|||
### Shutdown Simulator |
|||
|
|||
```bash |
|||
# Shutdown specific device |
|||
xcrun simctl shutdown "iPhone 15 Pro" |
|||
|
|||
# Shutdown all devices |
|||
xcrun simctl shutdown all |
|||
``` |
|||
|
|||
### Reset Simulator |
|||
|
|||
```bash |
|||
# Erase all content and settings |
|||
xcrun simctl erase "iPhone 15 Pro" |
|||
|
|||
# Reset and boot |
|||
xcrun simctl erase "iPhone 15 Pro" && xcrun simctl boot "iPhone 15 Pro" |
|||
``` |
|||
|
|||
### Delete App from Simulator |
|||
|
|||
```bash |
|||
# Uninstall app (replace with correct bundle ID) |
|||
xcrun simctl uninstall booted com.timesafari.dailynotification |
|||
# OR for Vue 3 test app: |
|||
xcrun simctl uninstall booted com.timesafari.dailynotification.test |
|||
|
|||
# Or reset entire simulator |
|||
xcrun simctl erase booted |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Comparison: Native iOS App vs Vue 3 Test App |
|||
|
|||
| Feature | Native iOS App (`ios/App`) | Vue 3 Test App (`test-apps/...`) | |
|||
|---------|---------------------------|----------------------------------| |
|||
| **Purpose** | Quick plugin development testing | Comprehensive testing with UI | |
|||
| **Frontend** | Simple HTML/Capacitor | Vue 3 with full UI | |
|||
| **Build Steps** | Plugin + iOS build | Plugin + Vue build + iOS build | |
|||
| **Capacitor Sync** | Not required | Required (`npx cap sync ios`) | |
|||
| **Best For** | Quick native testing | Full integration testing | |
|||
| **Bundle ID** | `com.timesafari.dailynotification` | `com.timesafari.dailynotification.test` | |
|||
|
|||
--- |
|||
|
|||
## Comparison with Android |
|||
|
|||
| Task | Android Native App | iOS Native App | Vue 3 Test App (iOS) | |
|||
|------|-------------------|----------------|---------------------| |
|||
| List devices | `emulator -list-avds` | `xcrun simctl list devices` | `xcrun simctl list devices` | |
|||
| Boot device | `emulator -avd <name>` | `xcrun simctl boot <name>` | `xcrun simctl boot <name>` | |
|||
| Install app | `adb install <apk>` | `xcrun simctl install booted <app>` | `xcrun simctl install booted <app>` | |
|||
| Launch app | `adb shell am start` | `xcrun simctl launch booted <bundle>` | `xcrun simctl launch booted <bundle>` | |
|||
| View logs | `adb logcat` | `xcrun simctl spawn booted log stream` | `xcrun simctl spawn booted log stream` | |
|||
| Build command | `./gradlew assembleDebug` | `xcodebuild -workspace ...` | `xcodebuild -workspace ...` | |
|||
|
|||
--- |
|||
|
|||
## Troubleshooting |
|||
|
|||
### Simulator Won't Boot |
|||
|
|||
```bash |
|||
# Check Xcode command line tools |
|||
xcode-select -p |
|||
|
|||
# Reinstall command line tools |
|||
sudo xcode-select --reset |
|||
|
|||
# Verify simulator runtime |
|||
xcrun simctl runtime list |
|||
``` |
|||
|
|||
### Build Fails |
|||
|
|||
```bash |
|||
# Clean build folder |
|||
cd ios/App # or ios/App for Vue 3 app |
|||
xcodebuild clean -workspace App.xcworkspace -scheme App |
|||
|
|||
# Reinstall CocoaPods dependencies |
|||
cd ../.. # Back to ios/ directory |
|||
pod install --repo-update |
|||
|
|||
# Rebuild |
|||
cd App |
|||
xcodebuild -workspace App.xcworkspace -scheme App -configuration Debug |
|||
``` |
|||
|
|||
### App Won't Install |
|||
|
|||
```bash |
|||
# Check if simulator is booted |
|||
xcrun simctl list devices | grep Booted |
|||
|
|||
# Verify app path exists |
|||
ls -la ios/App/build/derivedData/Build/Products/Debug-iphonesimulator/ |
|||
|
|||
# Check bundle identifier |
|||
plutil -extract CFBundleIdentifier raw ios/App/App/Info.plist |
|||
``` |
|||
|
|||
### Vue 3 App: Web Assets Not Syncing |
|||
|
|||
```bash |
|||
# Rebuild web assets |
|||
cd test-apps/daily-notification-test |
|||
npm run build |
|||
|
|||
# Force sync |
|||
npx cap sync ios --force |
|||
|
|||
# Verify assets are synced |
|||
ls -la ios/App/App/public/ |
|||
``` |
|||
|
|||
### Logs Not Showing |
|||
|
|||
```bash |
|||
# Use Console.app for better log viewing |
|||
open -a Console |
|||
|
|||
# Or use log command with filters |
|||
log stream --predicate 'processImagePath contains "App"' --level debug --style compact |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Additional Resources |
|||
|
|||
- [Capacitor iOS Documentation](https://capacitorjs.com/docs/ios) |
|||
- [Xcode Command Line Tools](https://developer.apple.com/xcode/resources/) |
|||
- [Simulator Documentation](https://developer.apple.com/documentation/xcode/running-your-app-in-the-simulator-or-on-a-device) |
|||
|
|||
--- |
|||
|
|||
**Note**: iOS development requires macOS. This guide assumes you're running on a Mac with Xcode installed. |
|||
|
|||
**Key Distinction**: |
|||
- **`ios/App`** = Native iOS development app (simple, for quick testing) |
|||
- **`test-apps/daily-notification-test`** = Vue 3 test app (full-featured, for comprehensive testing) |
|||
|
|||
@ -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,16 @@ |
|||
#!/bin/bash |
|||
# Quick NVM setup script |
|||
|
|||
set -e |
|||
|
|||
echo "Installing NVM (Node Version Manager)..." |
|||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash |
|||
|
|||
echo "" |
|||
echo "NVM installed! Please run:" |
|||
echo " source ~/.zshrc" |
|||
echo "" |
|||
echo "Then install Node.js with:" |
|||
echo " nvm install --lts" |
|||
echo " nvm use --lts" |
|||
echo " nvm alias default node" |
|||
@ -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)" |
|||
|
|||
@ -0,0 +1,161 @@ |
|||
#!/bin/bash |
|||
# Native iOS Development App Build and Deploy Script |
|||
# Builds and deploys the ios/App development app to iOS Simulator |
|||
# Similar to android/app - a simple Capacitor app for plugin development |
|||
|
|||
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)" |
|||
|
|||
# Check if we're in the project root |
|||
if [ ! -d "$PROJECT_ROOT/ios/App" ]; then |
|||
log_error "ios/App directory not found" |
|||
log_info "This script must be run from the project root directory" |
|||
log_info "Usage: cd /path/to/daily-notification-plugin && ./scripts/build-and-deploy-native-ios.sh" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Check prerequisites |
|||
log_step "Checking prerequisites..." |
|||
|
|||
if ! command -v xcodebuild &> /dev/null; then |
|||
log_error "xcodebuild not found. Install Xcode command line tools:" |
|||
log_info " xcode-select --install" |
|||
exit 1 |
|||
fi |
|||
|
|||
if ! command -v pod &> /dev/null; then |
|||
log_error "CocoaPods not found. Install with:" |
|||
log_info " gem install cocoapods" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Get simulator device (default to iPhone 15 Pro) |
|||
SIMULATOR_DEVICE="${1:-iPhone 15 Pro}" |
|||
log_info "Using simulator: $SIMULATOR_DEVICE" |
|||
|
|||
# Boot simulator |
|||
log_step "Booting simulator..." |
|||
if xcrun simctl list devices | grep -q "$SIMULATOR_DEVICE.*Booted"; then |
|||
log_info "Simulator already booted" |
|||
else |
|||
# Try to boot the device |
|||
if xcrun simctl boot "$SIMULATOR_DEVICE" 2>/dev/null; then |
|||
log_info "✓ Simulator booted" |
|||
else |
|||
log_warn "Could not boot simulator automatically" |
|||
log_info "Opening Simulator app... (you may need to select device manually)" |
|||
open -a Simulator |
|||
sleep 5 |
|||
fi |
|||
fi |
|||
|
|||
# Build plugin |
|||
log_step "Building plugin..." |
|||
cd "$PROJECT_ROOT" |
|||
if ! ./scripts/build-native.sh --platform ios; then |
|||
log_error "Plugin build failed" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Install CocoaPods dependencies |
|||
log_step "Installing CocoaPods dependencies..." |
|||
cd "$PROJECT_ROOT/ios" |
|||
if [ ! -f "Podfile.lock" ] || [ "Podfile" -nt "Podfile.lock" ]; then |
|||
pod install |
|||
else |
|||
log_info "CocoaPods dependencies up to date" |
|||
fi |
|||
|
|||
# Build iOS app |
|||
log_step "Building native iOS development app..." |
|||
cd "$PROJECT_ROOT/ios/App" |
|||
WORKSPACE="App.xcworkspace" |
|||
SCHEME="App" |
|||
CONFIG="Debug" |
|||
SDK="iphonesimulator" |
|||
|
|||
if ! xcodebuild -workspace "$WORKSPACE" \ |
|||
-scheme "$SCHEME" \ |
|||
-configuration "$CONFIG" \ |
|||
-sdk "$SDK" \ |
|||
-destination "platform=iOS Simulator,name=$SIMULATOR_DEVICE" \ |
|||
-derivedDataPath build/derivedData \ |
|||
CODE_SIGN_IDENTITY="" \ |
|||
CODE_SIGNING_REQUIRED=NO \ |
|||
CODE_SIGNING_ALLOWED=NO \ |
|||
clean build; then |
|||
log_error "iOS app build failed" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Find built app |
|||
APP_PATH=$(find build/derivedData -name "*.app" -type d -path "*/Build/Products/*-iphonesimulator/*.app" | head -1) |
|||
|
|||
if [ -z "$APP_PATH" ]; then |
|||
log_error "Could not find built app" |
|||
log_info "Searching in: build/derivedData" |
|||
find build/derivedData -name "*.app" -type d 2>/dev/null | head -5 |
|||
exit 1 |
|||
fi |
|||
|
|||
log_info "Found app: $APP_PATH" |
|||
|
|||
# Install app on simulator |
|||
log_step "Installing app on simulator..." |
|||
if xcrun simctl install booted "$APP_PATH"; then |
|||
log_info "✓ App installed" |
|||
else |
|||
log_error "Failed to install app" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Get bundle identifier |
|||
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist 2>/dev/null || echo "com.timesafari.dailynotification") |
|||
log_info "Bundle ID: $BUNDLE_ID" |
|||
|
|||
# Launch app |
|||
log_step "Launching app..." |
|||
if xcrun simctl launch booted "$BUNDLE_ID"; then |
|||
log_info "✓ App launched" |
|||
else |
|||
log_warn "App may already be running" |
|||
fi |
|||
|
|||
log_info "" |
|||
log_info "✅ Build and deploy complete!" |
|||
log_info "" |
|||
log_info "To view logs:" |
|||
log_info " xcrun simctl spawn booted log stream" |
|||
log_info "" |
|||
log_info "To uninstall app:" |
|||
log_info " xcrun simctl uninstall booted $BUNDLE_ID" |
|||
log_info "" |
|||
log_info "Note: This is the native iOS development app (ios/App)" |
|||
log_info "For the Vue 3 test app, use: test-apps/daily-notification-test/scripts/build-and-deploy-ios.sh" |
|||
|
|||
@ -0,0 +1,250 @@ |
|||
#!/bin/bash |
|||
# Ruby Version Manager (rbenv) Setup Script |
|||
# Installs rbenv and Ruby 3.1+ for CocoaPods compatibility |
|||
|
|||
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" |
|||
} |
|||
|
|||
# Check if rbenv is already installed |
|||
if command -v rbenv &> /dev/null; then |
|||
log_info "rbenv is already installed" |
|||
RBENV_INSTALLED=true |
|||
else |
|||
log_step "Installing rbenv..." |
|||
RBENV_INSTALLED=false |
|||
fi |
|||
|
|||
# Install rbenv via Homebrew (recommended on macOS) |
|||
if [ "$RBENV_INSTALLED" = false ]; then |
|||
if command -v brew &> /dev/null; then |
|||
log_info "Installing rbenv via Homebrew..." |
|||
brew install rbenv ruby-build |
|||
|
|||
# Initialize rbenv in shell |
|||
if [ -n "$ZSH_VERSION" ]; then |
|||
SHELL_CONFIG="$HOME/.zshrc" |
|||
else |
|||
SHELL_CONFIG="$HOME/.bash_profile" |
|||
fi |
|||
|
|||
# Add rbenv initialization to shell config |
|||
if ! grep -q "rbenv init" "$SHELL_CONFIG" 2>/dev/null; then |
|||
log_info "Adding rbenv initialization to $SHELL_CONFIG..." |
|||
echo '' >> "$SHELL_CONFIG" |
|||
echo '# rbenv initialization' >> "$SHELL_CONFIG" |
|||
echo 'eval "$(rbenv init - zsh)"' >> "$SHELL_CONFIG" |
|||
fi |
|||
|
|||
# Load rbenv in current session |
|||
eval "$(rbenv init - zsh)" |
|||
|
|||
log_info "✓ rbenv installed successfully" |
|||
else |
|||
log_warn "Homebrew not found. Installing rbenv manually..." |
|||
|
|||
# Manual installation via git |
|||
if [ ! -d "$HOME/.rbenv" ]; then |
|||
git clone https://github.com/rbenv/rbenv.git ~/.rbenv |
|||
fi |
|||
|
|||
if [ ! -d "$HOME/.rbenv/plugins/ruby-build" ]; then |
|||
mkdir -p ~/.rbenv/plugins |
|||
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build |
|||
fi |
|||
|
|||
# Add to PATH |
|||
export PATH="$HOME/.rbenv/bin:$PATH" |
|||
eval "$(rbenv init - zsh)" |
|||
|
|||
# Add to shell config |
|||
if [ -n "$ZSH_VERSION" ]; then |
|||
SHELL_CONFIG="$HOME/.zshrc" |
|||
else |
|||
SHELL_CONFIG="$HOME/.bash_profile" |
|||
fi |
|||
|
|||
if ! grep -q "rbenv init" "$SHELL_CONFIG" 2>/dev/null; then |
|||
echo '' >> "$SHELL_CONFIG" |
|||
echo '# rbenv initialization' >> "$SHELL_CONFIG" |
|||
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> "$SHELL_CONFIG" |
|||
echo 'eval "$(rbenv init - zsh)"' >> "$SHELL_CONFIG" |
|||
fi |
|||
|
|||
log_info "✓ rbenv installed manually" |
|||
fi |
|||
fi |
|||
|
|||
# Reload shell config |
|||
log_step "Reloading shell configuration..." |
|||
if [ -n "$ZSH_VERSION" ]; then |
|||
source ~/.zshrc 2>/dev/null || true |
|||
else |
|||
source ~/.bash_profile 2>/dev/null || true |
|||
fi |
|||
|
|||
# Ensure rbenv is in PATH |
|||
export PATH="$HOME/.rbenv/bin:$PATH" |
|||
eval "$(rbenv init - zsh)" 2>/dev/null || eval "$(rbenv init - bash)" 2>/dev/null || true |
|||
|
|||
# Check current Ruby version |
|||
log_step "Checking current Ruby version..." |
|||
CURRENT_RUBY=$(ruby -v 2>/dev/null | cut -d' ' -f2 | cut -d. -f1,2) || CURRENT_RUBY="unknown" |
|||
|
|||
if [ "$CURRENT_RUBY" != "unknown" ]; then |
|||
RUBY_MAJOR=$(echo "$CURRENT_RUBY" | cut -d. -f1) |
|||
RUBY_MINOR=$(echo "$CURRENT_RUBY" | cut -d. -f2) |
|||
|
|||
if [ "$RUBY_MAJOR" -ge 3 ] && [ "$RUBY_MINOR" -ge 1 ]; then |
|||
log_info "✓ Ruby version $CURRENT_RUBY is already >= 3.1.0" |
|||
log_info "You can proceed with CocoaPods installation" |
|||
exit 0 |
|||
else |
|||
log_warn "Current Ruby version: $CURRENT_RUBY (needs 3.1.0+)" |
|||
fi |
|||
fi |
|||
|
|||
# Check if rbenv has a suitable Ruby version already installed |
|||
log_step "Checking installed Ruby versions..." |
|||
if rbenv versions | grep -qE "3\.(1|2|3|4)\."; then |
|||
INSTALLED_RUBY=$(rbenv versions | grep -E "3\.(1|2|3|4)\." | tail -1 | sed 's/^[[:space:]]*//' | cut -d' ' -f1) |
|||
log_info "Found installed Ruby version: $INSTALLED_RUBY" |
|||
|
|||
# Set as global if not already set |
|||
CURRENT_GLOBAL=$(rbenv global) |
|||
if [ "$CURRENT_GLOBAL" != "$INSTALLED_RUBY" ]; then |
|||
log_info "Setting $INSTALLED_RUBY as default..." |
|||
rbenv global "$INSTALLED_RUBY" |
|||
rbenv rehash |
|||
fi |
|||
|
|||
# Verify it works |
|||
export PATH="$HOME/.rbenv/bin:$PATH" |
|||
eval "$(rbenv init - zsh)" 2>/dev/null || eval "$(rbenv init - bash)" 2>/dev/null || true |
|||
|
|||
if ruby -rpsych -e "true" 2>/dev/null; then |
|||
VERIFIED_RUBY=$(ruby -v) |
|||
log_info "✓ Ruby $VERIFIED_RUBY is working correctly" |
|||
log_info "" |
|||
log_info "Ruby setup complete!" |
|||
log_info "" |
|||
log_info "Next steps:" |
|||
log_info " 1. Reload your shell: source ~/.zshrc" |
|||
log_info " 2. Verify Ruby: ruby -v" |
|||
log_info " 3. Install CocoaPods: gem install cocoapods" |
|||
exit 0 |
|||
else |
|||
log_warn "Installed Ruby $INSTALLED_RUBY found but psych extension not working" |
|||
log_warn "May need to reinstall Ruby or install libyaml dependencies" |
|||
fi |
|||
fi |
|||
|
|||
# Check for libyaml dependency (required for psych extension) |
|||
log_step "Checking for libyaml dependency..." |
|||
LIBYAML_FOUND=false |
|||
if command -v brew &> /dev/null; then |
|||
if brew list libyaml &> /dev/null; then |
|||
LIBYAML_FOUND=true |
|||
log_info "✓ libyaml found via Homebrew" |
|||
else |
|||
log_warn "libyaml not installed. Installing via Homebrew..." |
|||
if brew install libyaml; then |
|||
LIBYAML_FOUND=true |
|||
log_info "✓ libyaml installed successfully" |
|||
else |
|||
log_error "Failed to install libyaml via Homebrew" |
|||
fi |
|||
fi |
|||
else |
|||
# Check if libyaml headers exist in system locations |
|||
if find /usr/local /opt /Library -name "yaml.h" 2>/dev/null | grep -q yaml.h; then |
|||
LIBYAML_FOUND=true |
|||
log_info "✓ libyaml headers found in system" |
|||
else |
|||
log_warn "libyaml not found. Ruby installation may fail." |
|||
log_warn "Install libyaml via Homebrew: brew install libyaml" |
|||
log_warn "Or install via MacPorts: sudo port install libyaml" |
|||
fi |
|||
fi |
|||
|
|||
# List available Ruby versions |
|||
log_step "Fetching available Ruby versions..." |
|||
rbenv install --list | grep -E "^[[:space:]]*3\.[1-9]" | tail -5 || log_warn "Could not fetch Ruby versions list" |
|||
|
|||
# Install Ruby 3.1.0 (preferred for compatibility) |
|||
log_step "Installing Ruby 3.1.0..." |
|||
if rbenv install 3.1.0; then |
|||
log_info "✓ Ruby 3.1.0 installed successfully" |
|||
|
|||
# Set as global default |
|||
log_step "Setting Ruby 3.1.0 as default..." |
|||
rbenv global 3.1.0 |
|||
|
|||
# Verify installation |
|||
export PATH="$HOME/.rbenv/bin:$PATH" |
|||
eval "$(rbenv init - zsh)" 2>/dev/null || eval "$(rbenv init - bash)" 2>/dev/null || true |
|||
|
|||
NEW_RUBY=$(ruby -v) |
|||
log_info "✓ Current Ruby version: $NEW_RUBY" |
|||
|
|||
# Verify psych extension works |
|||
if ruby -rpsych -e "true" 2>/dev/null; then |
|||
log_info "✓ psych extension verified" |
|||
else |
|||
log_warn "psych extension may not be working correctly" |
|||
log_warn "This may affect CocoaPods installation" |
|||
fi |
|||
|
|||
# Rehash to make Ruby available |
|||
rbenv rehash |
|||
|
|||
log_info "" |
|||
log_info "Ruby setup complete!" |
|||
log_info "" |
|||
log_info "Next steps:" |
|||
log_info " 1. Reload your shell: source ~/.zshrc" |
|||
log_info " 2. Verify Ruby: ruby -v" |
|||
log_info " 3. Install CocoaPods: gem install cocoapods" |
|||
|
|||
else |
|||
log_error "Failed to install Ruby 3.1.0" |
|||
|
|||
if [ "$LIBYAML_FOUND" = false ]; then |
|||
log_error "" |
|||
log_error "Installation failed. This is likely due to missing libyaml dependency." |
|||
log_error "" |
|||
log_error "To fix:" |
|||
if command -v brew &> /dev/null; then |
|||
log_error " brew install libyaml" |
|||
else |
|||
log_error " Install Homebrew: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" |
|||
log_error " Then: brew install libyaml" |
|||
fi |
|||
log_error "" |
|||
log_error "After installing libyaml, run this script again." |
|||
else |
|||
log_error "Installation failed. Please check your internet connection and try again." |
|||
fi |
|||
exit 1 |
|||
fi |
|||
@ -0,0 +1,183 @@ |
|||
# iOS Build Process Quick Reference |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Date**: November 4, 2025 |
|||
|
|||
## Two Different Test Apps |
|||
|
|||
**Important**: There are two different iOS test apps: |
|||
|
|||
1. **Native iOS Development App** (`ios/App`) - Simple Capacitor app for quick plugin testing |
|||
2. **Vue 3 Test App** (`test-apps/daily-notification-test`) - Full-featured Vue 3 Capacitor app |
|||
|
|||
--- |
|||
|
|||
## Vue 3 Test App (`test-apps/daily-notification-test`) |
|||
|
|||
### 🚨 Critical Build Steps |
|||
|
|||
```bash |
|||
# 1. Build web assets |
|||
npm run build |
|||
|
|||
# 2. Sync with iOS project |
|||
npx cap sync ios |
|||
|
|||
# 3. Build iOS app |
|||
cd ios/App |
|||
xcodebuild -workspace App.xcworkspace \ |
|||
-scheme App \ |
|||
-configuration Debug \ |
|||
-sdk iphonesimulator \ |
|||
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
|||
CODE_SIGN_IDENTITY="" \ |
|||
CODE_SIGNING_REQUIRED=NO \ |
|||
CODE_SIGNING_ALLOWED=NO |
|||
|
|||
# 4. Install and launch |
|||
APP_PATH=$(find build/derivedData -name "*.app" -type d | head -1) |
|||
xcrun simctl install booted "$APP_PATH" |
|||
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
|||
xcrun simctl launch booted "$BUNDLE_ID" |
|||
``` |
|||
|
|||
### 🔄 Using Capacitor CLI (Simplest Method) |
|||
|
|||
```bash |
|||
cd test-apps/daily-notification-test |
|||
|
|||
# Build and run in one command |
|||
npx cap run ios |
|||
|
|||
# This handles: |
|||
# - Building web assets |
|||
# - Syncing with iOS |
|||
# - Building app |
|||
# - Installing on simulator |
|||
# - Launching app |
|||
``` |
|||
|
|||
### 🛠️ Automated Build Script |
|||
|
|||
```bash |
|||
cd test-apps/daily-notification-test |
|||
./scripts/build-and-deploy-ios.sh |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Native iOS Development App (`ios/App`) |
|||
|
|||
### 🚨 Critical Build Steps |
|||
|
|||
```bash |
|||
# 1. Build plugin |
|||
cd /path/to/daily-notification-plugin |
|||
./scripts/build-native.sh --platform ios |
|||
|
|||
# 2. Install CocoaPods dependencies |
|||
cd ios |
|||
pod install |
|||
|
|||
# 3. Build iOS app |
|||
cd App |
|||
xcodebuild -workspace App.xcworkspace \ |
|||
-scheme App \ |
|||
-configuration Debug \ |
|||
-sdk iphonesimulator \ |
|||
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
|||
CODE_SIGN_IDENTITY="" \ |
|||
CODE_SIGNING_REQUIRED=NO \ |
|||
CODE_SIGNING_ALLOWED=NO |
|||
|
|||
# 4. Install and launch |
|||
APP_PATH=$(find build/derivedData -name "*.app" -type d | head -1) |
|||
xcrun simctl install booted "$APP_PATH" |
|||
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
|||
xcrun simctl launch booted "$BUNDLE_ID" |
|||
``` |
|||
|
|||
### 🛠️ Automated Build Script |
|||
|
|||
```bash |
|||
cd /path/to/daily-notification-plugin |
|||
./scripts/build-and-deploy-native-ios.sh |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## ⚠️ iOS-Specific Requirements |
|||
|
|||
**Prerequisites:** |
|||
- macOS (required for iOS development) |
|||
- Xcode installed (`xcode-select --install`) |
|||
- CocoaPods installed (`gem install cocoapods`) |
|||
- iOS Simulator runtime installed |
|||
|
|||
**Common Issues:** |
|||
- Simulator not booted: `xcrun simctl boot "iPhone 15 Pro"` |
|||
- CocoaPods not installed: `sudo gem install cocoapods` |
|||
- Platform components missing: `xcodebuild -downloadPlatform iOS` |
|||
|
|||
## 🔍 Verification Checklist |
|||
|
|||
After build, verify: |
|||
|
|||
### For Vue 3 Test App: |
|||
- [ ] Simulator is booted (`xcrun simctl list devices | grep Booted`) |
|||
- [ ] CocoaPods dependencies installed (`cd ios && pod install`) |
|||
- [ ] Web assets synced (`npx cap sync ios`) |
|||
- [ ] App builds successfully (`xcodebuild ...`) |
|||
- [ ] App installs on simulator (`xcrun simctl install`) |
|||
- [ ] App launches (`xcrun simctl launch`) |
|||
|
|||
### For Native iOS App: |
|||
- [ ] Simulator is booted (`xcrun simctl list devices | grep Booted`) |
|||
- [ ] Plugin built (`./scripts/build-native.sh --platform ios`) |
|||
- [ ] CocoaPods dependencies installed (`cd ios && pod install`) |
|||
- [ ] App builds successfully (`xcodebuild ...`) |
|||
- [ ] App installs on simulator (`xcrun simctl install`) |
|||
- [ ] App launches (`xcrun simctl launch`) |
|||
|
|||
## 📱 Testing Commands |
|||
|
|||
```bash |
|||
# List available simulators |
|||
xcrun simctl list devices available |
|||
|
|||
# Boot simulator |
|||
xcrun simctl boot "iPhone 15 Pro" |
|||
|
|||
# Check if booted |
|||
xcrun simctl list devices | grep Booted |
|||
|
|||
# View logs |
|||
xcrun simctl spawn booted log stream |
|||
|
|||
# Uninstall app |
|||
xcrun simctl uninstall booted com.timesafari.dailynotification.test # Vue 3 app |
|||
xcrun simctl uninstall booted com.timesafari.dailynotification # Native app |
|||
|
|||
# Reset simulator |
|||
xcrun simctl erase booted |
|||
``` |
|||
|
|||
## 🐛 Common Issues |
|||
|
|||
| Issue | Symptom | Solution | |
|||
|-------|---------|----------| |
|||
| Simulator not found | `Unable to find destination` | Run `xcrun simctl list devices` to see available devices | |
|||
| CocoaPods error | `pod: command not found` | Install CocoaPods: `gem install cocoapods` | |
|||
| Build fails | `No such file or directory` | Run `pod install` in `ios/` directory | |
|||
| Signing error | `Code signing required` | Add `CODE_SIGNING_REQUIRED=NO` to xcodebuild command | |
|||
| App won't install | `Could not find application` | Verify app path exists and simulator is booted | |
|||
| Vue app: assets not syncing | App shows blank screen | Run `npm run build && npx cap sync ios` | |
|||
|
|||
--- |
|||
|
|||
**Remember**: |
|||
- **Native iOS App** (`ios/App`) = Quick plugin testing, no web build needed |
|||
- **Vue 3 Test App** (`test-apps/...`) = Full testing with UI, requires `npm run build` |
|||
|
|||
Use `npx cap run ios` in the Vue 3 test app directory for the simplest workflow! |
|||
|
|||
@ -0,0 +1,150 @@ |
|||
#!/bin/bash |
|||
# iOS Test App Build and Deploy Script |
|||
# Builds and deploys the DailyNotification test app to iOS Simulator |
|||
|
|||
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" |
|||
} |
|||
|
|||
# Check if we're in the test app directory |
|||
if [ ! -f "package.json" ] || [ ! -d "ios" ]; then |
|||
log_error "This script must be run from test-apps/daily-notification-test directory" |
|||
log_info "Usage: cd test-apps/daily-notification-test && ./scripts/build-and-deploy-ios.sh" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Check prerequisites |
|||
log_step "Checking prerequisites..." |
|||
|
|||
if ! command -v xcodebuild &> /dev/null; then |
|||
log_error "xcodebuild not found. Install Xcode command line tools:" |
|||
log_info " xcode-select --install" |
|||
exit 1 |
|||
fi |
|||
|
|||
if ! command -v pod &> /dev/null; then |
|||
log_error "CocoaPods not found. Install with:" |
|||
log_info " gem install cocoapods" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Get simulator device (default to iPhone 15 Pro) |
|||
SIMULATOR_DEVICE="${1:-iPhone 15 Pro}" |
|||
log_info "Using simulator: $SIMULATOR_DEVICE" |
|||
|
|||
# Boot simulator |
|||
log_step "Booting simulator..." |
|||
if xcrun simctl list devices | grep -q "$SIMULATOR_DEVICE.*Booted"; then |
|||
log_info "Simulator already booted" |
|||
else |
|||
# Try to boot the device |
|||
if xcrun simctl boot "$SIMULATOR_DEVICE" 2>/dev/null; then |
|||
log_info "✓ Simulator booted" |
|||
else |
|||
log_warn "Could not boot simulator automatically" |
|||
log_info "Opening Simulator app... (you may need to select device manually)" |
|||
open -a Simulator |
|||
sleep 5 |
|||
fi |
|||
fi |
|||
|
|||
# Build web assets |
|||
log_step "Building web assets..." |
|||
npm run build |
|||
|
|||
# Sync with iOS |
|||
log_step "Syncing with iOS project..." |
|||
if ! npx cap sync ios; then |
|||
log_error "Failed to sync with iOS project" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Install CocoaPods dependencies |
|||
log_step "Installing CocoaPods dependencies..." |
|||
cd ios/App |
|||
if [ ! -f "Podfile.lock" ] || [ "Podfile" -nt "Podfile.lock" ]; then |
|||
pod install |
|||
else |
|||
log_info "CocoaPods dependencies up to date" |
|||
fi |
|||
|
|||
# Build iOS app |
|||
log_step "Building iOS app..." |
|||
WORKSPACE="App.xcworkspace" |
|||
SCHEME="App" |
|||
CONFIG="Debug" |
|||
SDK="iphonesimulator" |
|||
|
|||
xcodebuild -workspace "$WORKSPACE" \ |
|||
-scheme "$SCHEME" \ |
|||
-configuration "$CONFIG" \ |
|||
-sdk "$SDK" \ |
|||
-destination "platform=iOS Simulator,name=$SIMULATOR_DEVICE" \ |
|||
-derivedDataPath build/derivedData \ |
|||
CODE_SIGN_IDENTITY="" \ |
|||
CODE_SIGNING_REQUIRED=NO \ |
|||
CODE_SIGNING_ALLOWED=NO \ |
|||
clean build |
|||
|
|||
# Find built app |
|||
APP_PATH=$(find build/derivedData -name "*.app" -type d -path "*/Build/Products/*-iphonesimulator/*.app" | head -1) |
|||
|
|||
if [ -z "$APP_PATH" ]; then |
|||
log_error "Could not find built app" |
|||
log_info "Searching in: build/derivedData" |
|||
exit 1 |
|||
fi |
|||
|
|||
log_info "Found app: $APP_PATH" |
|||
|
|||
# Install app on simulator |
|||
log_step "Installing app on simulator..." |
|||
if xcrun simctl install booted "$APP_PATH"; then |
|||
log_info "✓ App installed" |
|||
else |
|||
log_error "Failed to install app" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Get bundle identifier |
|||
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist 2>/dev/null || echo "com.timesafari.dailynotification.test") |
|||
log_info "Bundle ID: $BUNDLE_ID" |
|||
|
|||
# Launch app |
|||
log_step "Launching app..." |
|||
if xcrun simctl launch booted "$BUNDLE_ID"; then |
|||
log_info "✓ App launched" |
|||
else |
|||
log_warn "App may already be running" |
|||
fi |
|||
|
|||
log_info "" |
|||
log_info "✅ Build and deploy complete!" |
|||
log_info "" |
|||
log_info "To view logs:" |
|||
log_info " xcrun simctl spawn booted log stream" |
|||
log_info "" |
|||
log_info "To uninstall app:" |
|||
log_info " xcrun simctl uninstall booted $BUNDLE_ID" |
|||
|
|||
Loading…
Reference in new issue