Browse Source

feat(ios): add iOS deployment support and web assets parity

Add comprehensive iOS build and deployment infrastructure with command-line
tooling, documentation, and web assets synchronization.

Changes:
- Update Capacitor dependencies to v6.0 in podspec
- Add iOS build support to build-native.sh with NVM integration
- Sync iOS web assets to match www/ source directory
- Create deployment scripts for both native iOS app and Vue 3 test app
- Add comprehensive iOS simulator deployment documentation
- Document web assets parity requirements between Android and iOS

This enables:
- Command-line iOS builds without Xcode UI
- Automated deployment to iOS simulators
- Consistent web assets across platforms
- Clear separation between native iOS app (ios/App) and Vue 3 test app

Files modified:
- ios/DailyNotificationPlugin.podspec (Capacitor 6.0)
- ios/App/App/public/index.html (synced from www/)
- scripts/build-native.sh (iOS build support)

Files added:
- docs/WEB_ASSETS_PARITY.md
- docs/standalone-ios-simulator-guide.md
- scripts/build-and-deploy-native-ios.sh
- test-apps/daily-notification-test/docs/IOS_BUILD_QUICK_REFERENCE.md
- test-apps/daily-notification-test/scripts/build-and-deploy-ios.sh
ios-implementation
Matthew Raymer 2 days ago
parent
commit
4be87acc14
  1. 64
      docs/WEB_ASSETS_PARITY.md
  2. 548
      docs/standalone-ios-simulator-guide.md
  3. 616
      ios/App/App/public/index.html
  4. 4
      ios/DailyNotificationPlugin.podspec
  5. 14
      ios/Podfile.lock
  6. 16
      nvm-install.sh
  7. 12
      package-lock.json
  8. 161
      scripts/build-and-deploy-native-ios.sh
  9. 377
      scripts/build-native.sh
  10. 250
      scripts/setup-ruby.sh
  11. 183
      test-apps/daily-notification-test/docs/IOS_BUILD_QUICK_REFERENCE.md
  12. 150
      test-apps/daily-notification-test/scripts/build-and-deploy-ios.sh

64
docs/WEB_ASSETS_PARITY.md

@ -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.

548
docs/standalone-ios-simulator-guide.md

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

616
ios/App/App/public/index.html

@ -3,6 +3,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>DailyNotification Plugin Test</title> <title>DailyNotification Plugin Test</title>
<style> <style>
body { body {
@ -14,98 +17,627 @@
color: white; color: white;
} }
.container { .container {
max-width: 600px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
text-align: center;
} }
h1 { h1 {
text-align: center;
margin-bottom: 30px; margin-bottom: 30px;
font-size: 2.5em; font-size: 2.5em;
} }
.section {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
margin: 20px 0;
}
.section h2 {
margin-top: 0;
color: #ffd700;
}
.button { .button {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3); border: 2px solid rgba(255, 255, 255, 0.3);
color: white; color: white;
padding: 15px 30px; padding: 12px 24px;
margin: 10px; margin: 8px;
border-radius: 25px; border-radius: 20px;
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 14px;
transition: all 0.3s ease; transition: all 0.3s ease;
display: inline-block;
} }
.button:hover { .button:hover {
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px); transform: translateY(-2px);
} }
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.status { .status {
margin-top: 30px; margin-top: 15px;
padding: 20px; padding: 15px;
background: rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.2);
border-radius: 10px; border-radius: 8px;
font-family: monospace; font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
}
.success { color: #4CAF50; }
.error { color: #f44336; }
.warning { color: #ff9800; }
.info { color: #2196F3; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 15px 0;
}
.input-group {
margin: 10px 0;
}
.input-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.input-group input, .input-group select {
width: 100%;
padding: 8px;
border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.1);
color: white;
}
.input-group input::placeholder {
color: rgba(255, 255, 255, 0.7);
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>🔔 DailyNotification Plugin Test</h1> <h1>🔔 DailyNotification Plugin Test</h1>
<p>Test the DailyNotification plugin functionality</p>
<button class="button" onclick="testPlugin()">Test Plugin</button> <!-- Plugin Status Section -->
<div class="section">
<h2>📊 Plugin Status</h2>
<div class="grid">
<button class="button" onclick="checkPluginAvailability()">Check Availability</button>
<button class="button" onclick="getNotificationStatus()">Get Status</button>
<button class="button" onclick="checkPermissions()">Check Permissions</button>
<button class="button" onclick="getBatteryStatus()">Battery Status</button>
</div>
<div id="status" class="status">Ready to test...</div>
</div>
<!-- Permission Management Section -->
<div class="section">
<h2>🔐 Permission Management</h2>
<div class="grid">
<button class="button" onclick="requestPermissions()">Request Permissions</button>
<button class="button" onclick="requestExactAlarmPermission()">Request Exact Alarm</button>
<button class="button" onclick="openExactAlarmSettings()">Open Settings</button>
<button class="button" onclick="requestBatteryOptimizationExemption()">Battery Exemption</button>
</div>
</div>
<!-- Notification Scheduling Section -->
<div class="section">
<h2>⏰ Notification Scheduling</h2>
<div class="input-group">
<label for="notificationUrl">Content URL:</label>
<input type="text" id="notificationUrl" placeholder="https://api.example.com/daily-content" value="https://api.example.com/daily-content">
</div>
<div class="input-group">
<label for="notificationTime">Schedule Time:</label>
<input type="time" id="notificationTime" value="09:00">
</div>
<div class="input-group">
<label for="notificationTitle">Title:</label>
<input type="text" id="notificationTitle" placeholder="Daily Notification" value="Daily Notification">
</div>
<div class="input-group">
<label for="notificationBody">Body:</label>
<input type="text" id="notificationBody" placeholder="Your daily content is ready!" value="Your daily content is ready!">
</div>
<div class="grid">
<button class="button" onclick="scheduleNotification()">Schedule Notification</button>
<button class="button" onclick="cancelAllNotifications()">Cancel All</button>
<button class="button" onclick="getLastNotification()">Get Last</button>
</div>
</div>
<!-- Configuration Section -->
<div class="section">
<h2>⚙️ Plugin Configuration</h2>
<div class="input-group">
<label for="configUrl">Fetch URL:</label>
<input type="text" id="configUrl" placeholder="https://api.example.com/content" value="https://api.example.com/content">
</div>
<div class="input-group">
<label for="configTime">Schedule Time:</label>
<input type="time" id="configTime" value="09:00">
</div>
<div class="input-group">
<label for="configRetryCount">Retry Count:</label>
<input type="number" id="configRetryCount" value="3" min="0" max="10">
</div>
<div class="grid">
<button class="button" onclick="configurePlugin()">Configure Plugin</button> <button class="button" onclick="configurePlugin()">Configure Plugin</button>
<button class="button" onclick="checkStatus()">Check Status</button> <button class="button" onclick="updateSettings()">Update Settings</button>
</div>
</div>
<div id="status" class="status"> <!-- Advanced Features Section -->
Ready to test... <div class="section">
<h2>🚀 Advanced Features</h2>
<div class="grid">
<button class="button" onclick="getExactAlarmStatus()">Exact Alarm Status</button>
<button class="button" onclick="getRebootRecoveryStatus()">Reboot Recovery</button>
<button class="button" onclick="getRollingWindowStats()">Rolling Window</button>
<button class="button" onclick="maintainRollingWindow()">Maintain Window</button>
<button class="button" onclick="getContentCache()">Content Cache</button>
<button class="button" onclick="clearContentCache()">Clear Cache</button>
<button class="button" onclick="getContentHistory()">Content History</button>
<button class="button" onclick="getDualScheduleStatus()">Dual Schedule</button>
</div>
</div> </div>
</div> </div>
<script type="module"> <script>
import { Capacitor } from '@capacitor/core'; console.log('🔔 DailyNotification Plugin Test Interface Loading...');
import { DailyNotification } from '@timesafari/daily-notification-plugin';
window.Capacitor = Capacitor; // Global variables
window.DailyNotification = DailyNotification; let plugin = null;
let isPluginAvailable = false;
window.testPlugin = async function() { // Initialize plugin on page load
const status = document.getElementById('status'); document.addEventListener('DOMContentLoaded', async function() {
status.innerHTML = 'Testing plugin...'; console.log('📱 DOM loaded, initializing plugin...');
await initializePlugin();
});
// Initialize the real DailyNotification plugin
async function initializePlugin() {
try { try {
// Plugin is loaded and ready // Try to access the real plugin through Capacitor
status.innerHTML = 'Plugin is loaded and ready!'; if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DailyNotification) {
plugin = window.Capacitor.Plugins.DailyNotification;
isPluginAvailable = true;
console.log('✅ Real DailyNotification plugin found!');
updateStatus('success', '✅ Real DailyNotification plugin loaded successfully!');
} else {
// Fallback to mock for development
console.log('⚠️ Real plugin not available, using mock for development');
plugin = createMockPlugin();
isPluginAvailable = false;
updateStatus('warning', '⚠️ Using mock plugin (real plugin not available)');
}
} catch (error) { } catch (error) {
status.innerHTML = `Plugin test failed: ${error.message}`; console.error('❌ Plugin initialization failed:', error);
updateStatus('error', `❌ Plugin initialization failed: ${error.message}`);
}
}
// Create mock plugin for development/testing
function createMockPlugin() {
return {
configure: async (options) => {
console.log('Mock configure called with:', options);
return Promise.resolve();
},
getNotificationStatus: async () => {
return Promise.resolve({
isEnabled: true,
isScheduled: true,
lastNotificationTime: Date.now() - 86400000,
nextNotificationTime: Date.now() + 3600000,
pending: 1,
settings: { url: 'https://api.example.com/content', time: '09:00' },
error: null
});
},
checkPermissions: async () => {
return Promise.resolve({
notifications: 'granted',
backgroundRefresh: 'granted',
alert: true,
badge: true,
sound: true
});
},
requestPermissions: async () => {
return Promise.resolve({
notifications: 'granted',
backgroundRefresh: 'granted',
alert: true,
badge: true,
sound: true
});
},
scheduleDailyNotification: async (options) => {
console.log('Mock scheduleDailyNotification called with:', options);
return Promise.resolve();
},
cancelAllNotifications: async () => {
console.log('Mock cancelAllNotifications called');
return Promise.resolve();
},
getLastNotification: async () => {
return Promise.resolve({
id: 'mock-123',
title: 'Mock Notification',
body: 'This is a mock notification',
timestamp: Date.now() - 3600000,
url: 'https://example.com'
});
},
getBatteryStatus: async () => {
return Promise.resolve({
level: 85,
isCharging: false,
powerState: 1,
isOptimizationExempt: false
});
},
getExactAlarmStatus: async () => {
return Promise.resolve({
supported: true,
enabled: true,
canSchedule: true,
fallbackWindow: '±15 minutes'
});
},
requestExactAlarmPermission: async () => {
console.log('Mock requestExactAlarmPermission called');
return Promise.resolve();
},
openExactAlarmSettings: async () => {
console.log('Mock openExactAlarmSettings called');
return Promise.resolve();
},
requestBatteryOptimizationExemption: async () => {
console.log('Mock requestBatteryOptimizationExemption called');
return Promise.resolve();
},
getRebootRecoveryStatus: async () => {
return Promise.resolve({
inProgress: false,
lastRecoveryTime: Date.now() - 86400000,
timeSinceLastRecovery: 86400000,
recoveryNeeded: false
});
},
getRollingWindowStats: async () => {
return Promise.resolve({
stats: 'Window: 7 days, Notifications: 5, Success rate: 100%',
maintenanceNeeded: false,
timeUntilNextMaintenance: 3600000
});
},
maintainRollingWindow: async () => {
console.log('Mock maintainRollingWindow called');
return Promise.resolve();
},
getContentCache: async () => {
return Promise.resolve({
'cache-key-1': { content: 'Mock cached content', timestamp: Date.now() },
'cache-key-2': { content: 'Another mock item', timestamp: Date.now() - 3600000 }
});
},
clearContentCache: async () => {
console.log('Mock clearContentCache called');
return Promise.resolve();
},
getContentHistory: async () => {
return Promise.resolve([
{ id: '1', timestamp: Date.now() - 86400000, success: true, content: 'Mock content 1' },
{ id: '2', timestamp: Date.now() - 172800000, success: true, content: 'Mock content 2' }
]);
},
getDualScheduleStatus: async () => {
return Promise.resolve({
isActive: true,
contentSchedule: { nextRun: Date.now() + 3600000, isEnabled: true },
userSchedule: { nextRun: Date.now() + 7200000, isEnabled: true },
lastContentFetch: Date.now() - 3600000,
lastUserNotification: Date.now() - 7200000
});
} }
}; };
}
window.configurePlugin = async function() { // Utility function to update status display
const status = document.getElementById('status'); function updateStatus(type, message) {
status.innerHTML = 'Configuring plugin...'; const statusEl = document.getElementById('status');
statusEl.className = `status ${type}`;
statusEl.textContent = message;
console.log(`[${type.toUpperCase()}] ${message}`);
}
// Plugin availability check
async function checkPluginAvailability() {
updateStatus('info', '🔍 Checking plugin availability...');
try { try {
await DailyNotification.configure({ if (plugin) {
fetchUrl: 'https://api.example.com/daily-content', updateStatus('success', `✅ Plugin available: ${isPluginAvailable ? 'Real plugin' : 'Mock plugin'}`);
scheduleTime: '09:00', } else {
enableNotifications: true updateStatus('error', '❌ Plugin not available');
}); }
status.innerHTML = 'Plugin configured successfully!';
} catch (error) { } catch (error) {
status.innerHTML = `Configuration failed: ${error.message}`; updateStatus('error', `❌ Availability check failed: ${error.message}`);
}
}
// Get notification status
async function getNotificationStatus() {
updateStatus('info', '📊 Getting notification status...');
try {
const status = await plugin.getNotificationStatus();
updateStatus('success', `📊 Status: ${JSON.stringify(status, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Status check failed: ${error.message}`);
}
}
// Check permissions
async function checkPermissions() {
updateStatus('info', '🔐 Checking permissions...');
try {
const permissions = await plugin.checkPermissions();
updateStatus('success', `🔐 Permissions: ${JSON.stringify(permissions, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Permission check failed: ${error.message}`);
}
}
// Request permissions
async function requestPermissions() {
updateStatus('info', '🔐 Requesting permissions...');
try {
const result = await plugin.requestPermissions();
updateStatus('success', `🔐 Permission result: ${JSON.stringify(result, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Permission request failed: ${error.message}`);
}
}
// Get battery status
async function getBatteryStatus() {
updateStatus('info', '🔋 Getting battery status...');
try {
const battery = await plugin.getBatteryStatus();
updateStatus('success', `🔋 Battery: ${JSON.stringify(battery, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Battery check failed: ${error.message}`);
}
}
// Schedule notification
async function scheduleNotification() {
updateStatus('info', '⏰ Scheduling notification...');
try {
const timeInput = document.getElementById('notificationTime').value;
const [hours, minutes] = timeInput.split(':');
const now = new Date();
const scheduledTime = new Date();
scheduledTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
// If scheduled time is in the past, schedule for tomorrow
if (scheduledTime <= now) {
scheduledTime.setDate(scheduledTime.getDate() + 1);
} }
// Calculate prefetch time (5 minutes before notification)
const prefetchTime = new Date(scheduledTime.getTime() - 300000); // 5 minutes
const prefetchTimeReadable = prefetchTime.toLocaleTimeString();
const notificationTimeReadable = scheduledTime.toLocaleTimeString();
const prefetchTimeString = prefetchTime.getHours().toString().padStart(2, '0') + ':' +
prefetchTime.getMinutes().toString().padStart(2, '0');
const notificationTimeString = scheduledTime.getHours().toString().padStart(2, '0') + ':' +
scheduledTime.getMinutes().toString().padStart(2, '0');
const options = {
url: document.getElementById('notificationUrl').value,
time: timeInput,
title: document.getElementById('notificationTitle').value,
body: document.getElementById('notificationBody').value,
sound: true,
priority: 'high'
}; };
await plugin.scheduleDailyNotification(options);
updateStatus('success', `✅ Notification scheduled!<br>` +
`📥 Prefetch: ${prefetchTimeReadable} (${prefetchTimeString})<br>` +
`🔔 Notification: ${notificationTimeReadable} (${notificationTimeString})`);
} catch (error) {
updateStatus('error', `❌ Scheduling failed: ${error.message}`);
}
}
// Cancel all notifications
async function cancelAllNotifications() {
updateStatus('info', '❌ Cancelling all notifications...');
try {
await plugin.cancelAllNotifications();
updateStatus('success', '❌ All notifications cancelled');
} catch (error) {
updateStatus('error', `❌ Cancel failed: ${error.message}`);
}
}
window.checkStatus = async function() { // Get last notification
const status = document.getElementById('status'); async function getLastNotification() {
status.innerHTML = 'Checking plugin status...'; updateStatus('info', '📱 Getting last notification...');
try {
const notification = await plugin.getLastNotification();
updateStatus('success', `📱 Last notification: ${JSON.stringify(notification, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Get last notification failed: ${error.message}`);
}
}
// Configure plugin
async function configurePlugin() {
updateStatus('info', '⚙️ Configuring plugin...');
try { try {
const result = await DailyNotification.getStatus(); const config = {
status.innerHTML = `Plugin status: ${JSON.stringify(result, null, 2)}`; fetchUrl: document.getElementById('configUrl').value,
scheduleTime: document.getElementById('configTime').value,
retryCount: parseInt(document.getElementById('configRetryCount').value),
enableNotifications: true,
offlineFallback: true
};
await plugin.configure(config);
updateStatus('success', `⚙️ Plugin configured: ${JSON.stringify(config, null, 2)}`);
} catch (error) { } catch (error) {
status.innerHTML = `Status check failed: ${error.message}`; updateStatus('error', `❌ Configuration failed: ${error.message}`);
}
} }
// Update settings
async function updateSettings() {
updateStatus('info', '⚙️ Updating settings...');
try {
const settings = {
url: document.getElementById('configUrl').value,
time: document.getElementById('configTime').value,
retryCount: parseInt(document.getElementById('configRetryCount').value),
sound: true,
priority: 'high'
}; };
await plugin.updateSettings(settings);
updateStatus('success', `⚙️ Settings updated: ${JSON.stringify(settings, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Settings update failed: ${error.message}`);
}
}
// Get exact alarm status
async function getExactAlarmStatus() {
updateStatus('info', '⏰ Getting exact alarm status...');
try {
const status = await plugin.getExactAlarmStatus();
updateStatus('success', `⏰ Exact alarm status: ${JSON.stringify(status, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Exact alarm check failed: ${error.message}`);
}
}
// Request exact alarm permission
async function requestExactAlarmPermission() {
updateStatus('info', '⏰ Requesting exact alarm permission...');
try {
await plugin.requestExactAlarmPermission();
updateStatus('success', '⏰ Exact alarm permission requested');
} catch (error) {
updateStatus('error', `❌ Exact alarm permission request failed: ${error.message}`);
}
}
// Open exact alarm settings
async function openExactAlarmSettings() {
updateStatus('info', '⚙️ Opening exact alarm settings...');
try {
await plugin.openExactAlarmSettings();
updateStatus('success', '⚙️ Exact alarm settings opened');
} catch (error) {
updateStatus('error', `❌ Open settings failed: ${error.message}`);
}
}
// Request battery optimization exemption
async function requestBatteryOptimizationExemption() {
updateStatus('info', '🔋 Requesting battery optimization exemption...');
try {
await plugin.requestBatteryOptimizationExemption();
updateStatus('success', '🔋 Battery optimization exemption requested');
} catch (error) {
updateStatus('error', `❌ Battery exemption request failed: ${error.message}`);
}
}
// Get reboot recovery status
async function getRebootRecoveryStatus() {
updateStatus('info', '🔄 Getting reboot recovery status...');
try {
const status = await plugin.getRebootRecoveryStatus();
updateStatus('success', `🔄 Reboot recovery status: ${JSON.stringify(status, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Reboot recovery check failed: ${error.message}`);
}
}
// Get rolling window stats
async function getRollingWindowStats() {
updateStatus('info', '📊 Getting rolling window stats...');
try {
const stats = await plugin.getRollingWindowStats();
updateStatus('success', `📊 Rolling window stats: ${JSON.stringify(stats, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Rolling window stats failed: ${error.message}`);
}
}
// Maintain rolling window
async function maintainRollingWindow() {
updateStatus('info', '🔧 Maintaining rolling window...');
try {
await plugin.maintainRollingWindow();
updateStatus('success', '🔧 Rolling window maintenance completed');
} catch (error) {
updateStatus('error', `❌ Rolling window maintenance failed: ${error.message}`);
}
}
// Get content cache
async function getContentCache() {
updateStatus('info', '💾 Getting content cache...');
try {
const cache = await plugin.getContentCache();
updateStatus('success', `💾 Content cache: ${JSON.stringify(cache, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Content cache check failed: ${error.message}`);
}
}
// Clear content cache
async function clearContentCache() {
updateStatus('info', '🗑️ Clearing content cache...');
try {
await plugin.clearContentCache();
updateStatus('success', '🗑️ Content cache cleared');
} catch (error) {
updateStatus('error', `❌ Clear cache failed: ${error.message}`);
}
}
// Get content history
async function getContentHistory() {
updateStatus('info', '📚 Getting content history...');
try {
const history = await plugin.getContentHistory();
updateStatus('success', `📚 Content history: ${JSON.stringify(history, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Content history check failed: ${error.message}`);
}
}
// Get dual schedule status
async function getDualScheduleStatus() {
updateStatus('info', '🔄 Getting dual schedule status...');
try {
const status = await plugin.getDualScheduleStatus();
updateStatus('success', `🔄 Dual schedule status: ${JSON.stringify(status, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Dual schedule check failed: ${error.message}`);
}
}
console.log('🔔 DailyNotification Plugin Test Interface Loaded Successfully!');
</script> </script>
</body> </body>
</html> </html>

4
ios/DailyNotificationPlugin.podspec

@ -8,8 +8,8 @@ Pod::Spec.new do |s|
s.source = { :git => 'https://github.com/timesafari/daily-notification-plugin.git', :tag => s.version.to_s } s.source = { :git => 'https://github.com/timesafari/daily-notification-plugin.git', :tag => s.version.to_s }
s.source_files = 'Plugin/**/*.{swift,h,m,c,cc,mm,cpp}' s.source_files = 'Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
s.ios.deployment_target = '13.0' s.ios.deployment_target = '13.0'
s.dependency 'Capacitor', '~> 5.0.0' s.dependency 'Capacitor', '~> 6.0'
s.dependency 'CapacitorCordova', '~> 5.0.0' s.dependency 'CapacitorCordova', '~> 6.0'
s.swift_version = '5.1' s.swift_version = '5.1'
s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) COCOAPODS=1' } s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) COCOAPODS=1' }
s.deprecated = false s.deprecated = false

14
ios/Podfile.lock

@ -1,10 +1,10 @@
PODS: PODS:
- Capacitor (5.0.0): - Capacitor (6.2.1):
- CapacitorCordova - CapacitorCordova
- CapacitorCordova (5.0.0) - CapacitorCordova (6.2.1)
- DailyNotificationPlugin (1.0.0): - DailyNotificationPlugin (1.0.0):
- Capacitor (~> 5.0.0) - Capacitor (~> 6.0)
- CapacitorCordova (~> 5.0.0) - CapacitorCordova (~> 6.0)
DEPENDENCIES: DEPENDENCIES:
- "Capacitor (from `../node_modules/@capacitor/ios`)" - "Capacitor (from `../node_modules/@capacitor/ios`)"
@ -20,9 +20,9 @@ EXTERNAL SOURCES:
:path: "." :path: "."
SPEC CHECKSUMS: SPEC CHECKSUMS:
Capacitor: ba8cd5cce13c6ab3c4faf7ef98487be481c9c1c8 Capacitor: 1e0d0e7330dea9f983b50da737d8918abcf273f8
CapacitorCordova: 4ea17670ee562680988a7ce9db68dee5160fe564 CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
DailyNotificationPlugin: 745a0606d51baec6fc9a025f1de1ade125ed193a DailyNotificationPlugin: 79f269b45580c89b044ece1cfe09293b7e974d98
PODFILE CHECKSUM: ac8c229d24347f6f83e67e6b95458e0b81e68f7c PODFILE CHECKSUM: ac8c229d24347f6f83e67e6b95458e0b81e68f7c

16
nvm-install.sh

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

12
package-lock.json

@ -100,6 +100,7 @@
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2", "@babel/code-frame": "^7.26.2",
@ -650,6 +651,7 @@
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz", "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
"integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==", "integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
@ -752,6 +754,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@ -775,6 +778,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -2926,6 +2930,7 @@
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0", "@typescript-eslint/types": "5.62.0",
@ -3129,6 +3134,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3544,6 +3550,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73", "electron-to-chromium": "^1.5.73",
@ -4693,6 +4700,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
@ -6250,6 +6258,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jest/core": "^29.7.0", "@jest/core": "^29.7.0",
"@jest/types": "^29.6.3", "@jest/types": "^29.6.3",
@ -7116,6 +7125,7 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cssstyle": "^4.2.1", "cssstyle": "^4.2.1",
"data-urls": "^5.0.0", "data-urls": "^5.0.0",
@ -9575,6 +9585,7 @@
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
@ -10527,6 +10538,7 @@
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"

161
scripts/build-and-deploy-native-ios.sh

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

377
scripts/build-native.sh

@ -7,6 +7,7 @@ set -e
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Logging functions # Logging functions
@ -22,6 +23,10 @@ log_error() {
echo -e "${RED}[ERROR]${NC} $1" echo -e "${RED}[ERROR]${NC} $1"
} }
log_step() {
echo -e "${BLUE}[STEP]${NC} $1"
}
# Validation functions # Validation functions
check_command() { check_command() {
if ! command -v $1 &> /dev/null; then if ! command -v $1 &> /dev/null; then
@ -31,9 +36,68 @@ check_command() {
} }
check_environment() { check_environment() {
# Check for required tools local platform=$1
# Initialize NVM if available (for Node.js version management)
if [ -s "$HOME/.nvm/nvm.sh" ]; then
log_info "Loading NVM..."
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion"
# Use default Node.js version if available
if [ -f "$NVM_DIR/alias/default" ]; then
DEFAULT_NODE=$(cat "$NVM_DIR/alias/default")
if [ -n "$DEFAULT_NODE" ]; then
nvm use default >/dev/null 2>&1 || true
fi
fi
# Use latest LTS if no default
if ! command -v node &> /dev/null; then
log_info "No default Node.js version set, using latest LTS..."
nvm use --lts >/dev/null 2>&1 || nvm install --lts >/dev/null 2>&1 || true
fi
fi
# Common checks
check_command "node" check_command "node"
check_command "npm" check_command "npm"
# Check Node.js version
NODE_VERSION=$(node -v | cut -d. -f1 | tr -d 'v')
if [ -z "$NODE_VERSION" ] || ! [[ "$NODE_VERSION" =~ ^[0-9]+$ ]]; then
log_error "Could not determine Node.js version"
log_error "Please install Node.js: nvm install --lts (if using NVM)"
exit 1
fi
if [ "$NODE_VERSION" -lt 14 ]; then
log_error "Node.js version 14 or higher is required (found: $NODE_VERSION)"
exit 1
fi
# Platform-specific checks
case $platform in
"android")
check_environment_android
;;
"ios")
check_environment_ios
;;
"all")
check_environment_android
check_environment_ios
;;
*)
log_error "Invalid platform: $platform"
exit 1
;;
esac
}
check_environment_android() {
log_step "Checking Android environment..."
check_command "java" check_command "java"
# Check for Gradle Wrapper instead of system gradle # Check for Gradle Wrapper instead of system gradle
@ -42,30 +106,97 @@ check_environment() {
exit 1 exit 1
fi fi
# Check Node.js version # Check Java version (more robust parsing)
NODE_VERSION=$(node -v | cut -d. -f1 | tr -d 'v') JAVA_VERSION_OUTPUT=$(java -version 2>&1 | head -n 1)
if [ "$NODE_VERSION" -lt 14 ]; then if [ -z "$JAVA_VERSION_OUTPUT" ]; then
log_error "Node.js version 14 or higher is required" log_error "Could not determine Java version"
exit 1
fi
# Try multiple parsing methods for different Java output formats
JAVA_VERSION=$(echo "$JAVA_VERSION_OUTPUT" | grep -oE 'version "([0-9]+)' | grep -oE '[0-9]+' | head -1)
# Fallback: try to extract from "openjdk version" or "java version" format
if [ -z "$JAVA_VERSION" ]; then
JAVA_VERSION=$(echo "$JAVA_VERSION_OUTPUT" | sed -E 's/.*version "([0-9]+).*/\1/' | head -1)
fi
# Validate we got a number
if [ -z "$JAVA_VERSION" ] || ! [[ "$JAVA_VERSION" =~ ^[0-9]+$ ]]; then
log_error "Could not parse Java version from: $JAVA_VERSION_OUTPUT"
log_error "Please ensure Java is installed correctly"
exit 1 exit 1
fi fi
# Check Java version
JAVA_VERSION=$(java -version 2>&1 | head -n 1 | cut -d'"' -f2 | cut -d. -f1)
if [ "$JAVA_VERSION" -lt 11 ]; then if [ "$JAVA_VERSION" -lt 11 ]; then
log_error "Java version 11 or higher is required" log_error "Java version 11 or higher is required (found: $JAVA_VERSION)"
exit 1 exit 1
fi fi
# Check for Android SDK # Check for Android SDK
if [ -z "$ANDROID_HOME" ]; then if [ -z "$ANDROID_HOME" ]; then
log_error "ANDROID_HOME environment variable is not set" log_error "ANDROID_HOME environment variable is not set"
log_error "Set it with: export ANDROID_HOME=/path/to/android/sdk"
exit 1
fi
log_info "✓ Android environment OK (Java $JAVA_VERSION)"
}
check_environment_ios() {
log_step "Checking iOS environment..."
# Check for Xcode command line tools
if ! command -v xcodebuild &> /dev/null; then
log_error "xcodebuild not found. Install Xcode Command Line Tools:"
log_error " xcode-select --install"
exit 1
fi
# Check for CocoaPods
if ! command -v pod &> /dev/null; then
log_error "CocoaPods not found. Install with:"
log_error " sudo gem install cocoapods"
exit 1
fi
# Check for Swift
if ! command -v swift &> /dev/null; then
log_error "Swift compiler not found"
exit 1 exit 1
fi fi
# Verify workspace exists
if [ ! -d "ios/DailyNotificationPlugin.xcworkspace" ]; then
log_error "iOS workspace not found: ios/DailyNotificationPlugin.xcworkspace"
exit 1
fi
log_info "✓ iOS environment OK"
} }
# Build functions # Build functions
build_typescript() { build_typescript() {
log_info "Building TypeScript..." log_info "Building TypeScript..."
# Ensure npm dependencies are installed
if [ ! -d "node_modules" ]; then
log_step "Installing npm dependencies..."
if ! npm install; then
log_error "Failed to install npm dependencies"
exit 1
fi
else
# Check if package.json changed (compare with package-lock.json)
if [ -f "package-lock.json" ] && [ "package.json" -nt "package-lock.json" ]; then
log_step "package.json changed, updating dependencies..."
if ! npm install; then
log_error "Failed to update npm dependencies"
exit 1
fi
fi
fi
npm run clean npm run clean
if ! npm run build; then if ! npm run build; then
log_error "TypeScript build failed" log_error "TypeScript build failed"
@ -149,33 +280,6 @@ build_android() {
# ============================================================================= # =============================================================================
# AUTOMATIC FIX: capacitor.build.gradle for Plugin Development Projects # AUTOMATIC FIX: capacitor.build.gradle for Plugin Development Projects
# ============================================================================= # =============================================================================
#
# PROBLEM: The capacitor.build.gradle file is auto-generated by Capacitor CLI
# and includes a line that tries to load a file that doesn't exist in plugin
# development projects:
# apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
#
# WHY THIS HAPPENS:
# - This file is generated by 'npx cap sync', 'npx cap update', etc.
# - It assumes a full Capacitor app with proper plugin integration
# - Plugin development projects don't have the full Capacitor setup
#
# THE FIX:
# - Comment out the problematic line to prevent build failures
# - Add explanatory comment about why it's commented out
# - This fix gets applied automatically every time the build script runs
#
# WHEN THIS FIX GETS OVERWRITTEN:
# - Running 'npx cap sync' will regenerate the file and remove our fix
# - Running 'npx cap update' will regenerate the file and remove our fix
# - Running 'npx cap add android' will regenerate the file and remove our fix
#
# HOW TO RESTORE THE FIX:
# - Run this build script again (it will reapply the fix automatically)
# - Or run: ./scripts/fix-capacitor-build.sh
# - Or manually comment out the problematic line
#
# =============================================================================
if [ -f "app/capacitor.build.gradle" ]; then if [ -f "app/capacitor.build.gradle" ]; then
if grep -q "^apply from: \"../capacitor-cordova-android-plugins/cordova.variables.gradle\"" "app/capacitor.build.gradle"; then if grep -q "^apply from: \"../capacitor-cordova-android-plugins/cordova.variables.gradle\"" "app/capacitor.build.gradle"; then
@ -237,6 +341,185 @@ build_android() {
cd .. cd ..
} }
build_ios() {
log_info "Building iOS..."
cd ios || exit 1
# Build configuration (define early for validation)
SCHEME="DailyNotificationPlugin"
CONFIG="Release"
WORKSPACE="DailyNotificationPlugin.xcworkspace"
# Install CocoaPods dependencies
log_step "Installing CocoaPods dependencies..."
if [ ! -f "Podfile.lock" ] || [ "Podfile" -nt "Podfile.lock" ] || [ ! -d "Pods" ] || [ ! -d "Pods/Target Support Files" ]; then
log_info "Podfile changed, Podfile.lock missing, or Pods incomplete - running pod install..."
if ! pod install --repo-update; then
log_error "Failed to install CocoaPods dependencies"
exit 1
fi
else
log_info "Podfile.lock is up to date and Pods directory exists, skipping pod install"
fi
# Quick Swift syntax validation (full validation happens during build)
log_step "Validating Swift syntax..."
cd Plugin
SWIFT_FILES=$(find . -name "*.swift" -type f 2>/dev/null)
if [ -z "$SWIFT_FILES" ]; then
log_warn "No Swift files found in Plugin directory"
else
# Use swiftc with iOS SDK for basic syntax check
IOS_SDK=$(xcrun --show-sdk-path --sdk iphoneos 2>/dev/null)
if [ -n "$IOS_SDK" ]; then
for swift_file in $SWIFT_FILES; do
# Quick syntax check without full compilation
if ! swiftc -sdk "$IOS_SDK" -target arm64-apple-ios16.0 -parse "$swift_file" 2>&1 | grep -q "error:"; then
continue
else
log_warn "Syntax check found issues in $swift_file (will be caught during build)"
# Don't exit - let xcodebuild catch real errors
fi
done
else
log_warn "Could not find iOS SDK, skipping syntax validation"
fi
fi
cd ..
# Clean build
log_step "Cleaning iOS build..."
xcodebuild clean \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIG" \
-sdk iphoneos \
-destination 'generic/platform=iOS' \
-quiet || {
log_warn "Clean failed or unnecessary, continuing..."
}
# Check if iOS device platform is available
log_step "Checking iOS device platform availability..."
BUILD_DEVICE=false
if xcodebuild -showsdks 2>&1 | grep -q "iOS.*iphoneos"; then
IOS_SDK_VERSION=$(xcrun --show-sdk-version --sdk iphoneos 2>&1)
log_info "Found iOS SDK: $IOS_SDK_VERSION"
# Check if platform components are installed by trying a dry-run
DRY_RUN_OUTPUT=$(xcodebuild -workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-destination 'generic/platform=iOS' \
-dry-run 2>&1)
if echo "$DRY_RUN_OUTPUT" | grep -q "iOS.*is not installed"; then
log_warn "iOS device platform components not installed"
log_info "To install iOS device platform components, run:"
log_info " xcodebuild -downloadPlatform iOS"
log_info "Or via Xcode: Settings > Components > iOS $IOS_SDK_VERSION"
log_info ""
log_info "Building for iOS Simulator instead (sufficient for plugin development)"
else
BUILD_DEVICE=true
fi
else
log_warn "iOS SDK not found"
fi
# Build for device (iOS) if available
if [ "$BUILD_DEVICE" = true ]; then
log_step "Building for iOS device (arm64)..."
BUILD_OUTPUT=$(xcodebuild build \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIG" \
-sdk iphoneos \
-destination 'generic/platform=iOS' \
-derivedDataPath build/derivedData \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
2>&1)
if echo "$BUILD_OUTPUT" | grep -q "error.*iOS.*is not installed"; then
log_warn "iOS device build failed - platform components not installed"
echo "$BUILD_OUTPUT" > /tmp/xcodebuild_device.log
log_info "Check build log: /tmp/xcodebuild_device.log"
BUILD_DEVICE=false
elif echo "$BUILD_OUTPUT" | grep -q "BUILD SUCCEEDED"; then
log_info "✓ iOS device build completed"
else
log_warn "iOS device build completed with warnings"
echo "$BUILD_OUTPUT" > /tmp/xcodebuild_device.log
fi
fi
# Build for simulator
log_step "Building for iOS simulator..."
SIMULATOR_BUILD_OUTPUT=$(xcodebuild build \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIG" \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath build/derivedData \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
2>&1)
if echo "$SIMULATOR_BUILD_OUTPUT" | grep -q "BUILD SUCCEEDED"; then
log_info "✓ iOS simulator build completed successfully"
elif echo "$SIMULATOR_BUILD_OUTPUT" | grep -q "error:"; then
log_error "iOS simulator build failed"
echo "$SIMULATOR_BUILD_OUTPUT" | grep -E "(error:|warning:)" | head -20
exit 1
else
log_warn "iOS simulator build completed with warnings"
echo "$SIMULATOR_BUILD_OUTPUT" | grep -E "(warning:|error:)" | head -10
fi
# Find built frameworks
DEVICE_FRAMEWORK=$(find build/derivedData -path "*/Build/Products/*-iphoneos/DailyNotificationPlugin.framework" -type d | head -1)
SIMULATOR_FRAMEWORK=$(find build/derivedData -path "*/Build/Products/*-iphonesimulator/DailyNotificationPlugin.framework" -type d | head -1)
if [ -n "$DEVICE_FRAMEWORK" ]; then
log_info "✓ Device framework: $DEVICE_FRAMEWORK"
fi
if [ -n "$SIMULATOR_FRAMEWORK" ]; then
log_info "✓ Simulator framework: $SIMULATOR_FRAMEWORK"
fi
# Create universal framework (optional)
if [ -n "$DEVICE_FRAMEWORK" ] && [ -n "$SIMULATOR_FRAMEWORK" ]; then
log_step "Creating universal framework..."
UNIVERSAL_DIR="build/universal"
mkdir -p "$UNIVERSAL_DIR"
# Copy device framework
cp -R "$DEVICE_FRAMEWORK" "$UNIVERSAL_DIR/"
# Create universal binary
UNIVERSAL_FRAMEWORK="$UNIVERSAL_DIR/DailyNotificationPlugin.framework/DailyNotificationPlugin"
if lipo -create \
"$DEVICE_FRAMEWORK/DailyNotificationPlugin" \
"$SIMULATOR_FRAMEWORK/DailyNotificationPlugin" \
-output "$UNIVERSAL_FRAMEWORK" 2>/dev/null; then
log_info "✓ Universal framework: $UNIVERSAL_DIR/DailyNotificationPlugin.framework"
else
log_warn "Universal framework creation failed (may not be needed)"
fi
fi
cd ..
log_info "iOS build completed successfully!"
}
# Main build process # Main build process
main() { main() {
log_info "Starting build process..." log_info "Starting build process..."
@ -249,15 +532,29 @@ main() {
BUILD_PLATFORM="$2" BUILD_PLATFORM="$2"
shift 2 shift 2
;; ;;
--help|-h)
echo "Usage: $0 [--platform PLATFORM]"
echo ""
echo "Options:"
echo " --platform PLATFORM Build platform: 'android', 'ios', or 'all' (default: all)"
echo ""
echo "Examples:"
echo " $0 --platform android # Build Android only"
echo " $0 --platform ios # Build iOS only"
echo " $0 --platform all # Build both platforms"
echo " $0 # Build both platforms (default)"
exit 0
;;
*) *)
log_error "Unknown option: $1" log_error "Unknown option: $1"
log_error "Use --help for usage information"
exit 1 exit 1
;; ;;
esac esac
done done
# Check environment # Check environment (platform-specific)
check_environment check_environment "$BUILD_PLATFORM"
# Build TypeScript # Build TypeScript
build_typescript build_typescript
@ -267,11 +564,15 @@ main() {
"android") "android")
build_android build_android
;; ;;
"ios")
build_ios
;;
"all") "all")
build_android build_android
build_ios
;; ;;
*) *)
log_error "Invalid platform: $BUILD_PLATFORM. Use 'android' or 'all'" log_error "Invalid platform: $BUILD_PLATFORM. Use 'android', 'ios', or 'all'"
exit 1 exit 1
;; ;;
esac esac

250
scripts/setup-ruby.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

183
test-apps/daily-notification-test/docs/IOS_BUILD_QUICK_REFERENCE.md

@ -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!

150
test-apps/daily-notification-test/scripts/build-and-deploy-ios.sh

@ -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…
Cancel
Save