diff --git a/BUILDING.md b/BUILDING.md index d80d702..c99891e 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -44,9 +44,11 @@ npx cap run android ## Prerequisites ### Required Software -- **Android Studio** (latest stable version) +- **Android Studio** (latest stable version) - for Android development - **Java 11+** (for Kotlin compilation) - **Android SDK** with API level 21+ +- **Xcode** (latest stable version) - for iOS development (macOS only) +- **Xcode Command Line Tools** - required for iOS builds (includes `xcodebuild`, `sqlite3`, etc.) - **Node.js** 16+ (for TypeScript compilation) - **npm** or **yarn** (for dependency management) @@ -54,11 +56,35 @@ npx cap run android - **Gradle Wrapper** (included in project) - **Kotlin** (configured in build.gradle) - **TypeScript** (for plugin interface) +- **CocoaPods** - for iOS dependency management + +### iOS-Specific Prerequisites + +**Xcode Command Line Tools** are required for iOS builds. The build script will verify these are installed: + +```bash +# Install Xcode Command Line Tools (if not already installed) +xcode-select --install +``` + +**Verification:** +```bash +# Check if Command Line Tools are configured +xcode-select -p + +# Verify xcodebuild is available +xcodebuild -version + +# Verify sqlite3 is available (part of Command Line Tools) +sqlite3 --version +``` + +**Note:** The build script automatically checks for Command Line Tools and will fail with clear error messages if they're missing. ### System Requirements - **RAM**: 4GB minimum, 8GB recommended - **Storage**: 2GB free space -- **OS**: Windows 10+, macOS 10.14+, or Linux +- **OS**: Windows 10+, macOS 10.14+, or Linux (iOS development requires macOS) ## Build Methods @@ -297,6 +323,8 @@ android/build/reports/tests/test/index.html ### iOS Native Build Process +**Prerequisites:** Ensure Xcode Command Line Tools are installed (see [Prerequisites](#prerequisites) section). The build script will verify this automatically. + #### 1. Navigate to iOS Directory ```bash cd ios @@ -307,6 +335,12 @@ cd ios pod install ``` +**Note:** If you encounter issues with `pod install`, ensure Xcode Command Line Tools are properly configured: +```bash +xcode-select --install # Install if missing +xcode-select -p # Verify installation path +``` + #### 3. Build Commands ```bash # Build using Xcode command line @@ -782,6 +816,13 @@ The project includes several automated build scripts in the `scripts/` directory ./scripts/build-native.sh --platform ios ./scripts/build-native.sh --verbose +# Clean build (removes all build artifacts and caches) +./scripts/clean-build.sh +./scripts/clean-build.sh --all # Also cleans caches and reinstalls dependencies +./scripts/clean-build.sh --clean-gradle-cache # Clean Gradle cache +./scripts/clean-build.sh --clean-derived-data # Clean Xcode DerivedData +./scripts/clean-build.sh --reinstall-node # Reinstall node_modules + # TimeSafari-specific builds node scripts/build-timesafari.js @@ -948,6 +989,28 @@ adb logcat | grep DailyNotification ## Troubleshooting +### Clean Build (First Step for Many Issues) + +If you encounter persistent build issues, try a clean build first: + +```bash +# Clean all build artifacts (recommended first step) +./scripts/clean-build.sh + +# Clean everything including caches (for stubborn issues) +./scripts/clean-build.sh --all + +# Then rebuild +./scripts/build-native.sh --platform all +``` + +**When to use clean-build:** +- Build errors that don't make sense +- Dependency conflicts +- Stale build artifacts +- After switching branches +- After updating dependencies + ### Common Issues #### Gradle Sync Failures @@ -1019,6 +1082,39 @@ File → Project Structure → SDK Location # Solution: Check Kotlin version in build.gradle ``` +#### iOS Build Issues +```bash +# Problem: "Xcode Command Line Tools not configured" +# Error: xcode-select -p fails or xcodebuild not found +# Solution: Install Command Line Tools +xcode-select --install + +# Verify installation +xcode-select -p +xcodebuild -version +sqlite3 --version + +# Problem: "sqlite3 not found" or linker errors with SQLite +# Solution: Ensure Command Line Tools are properly installed +# The build script checks for this automatically, but if you see linker errors: +xcode-select --install + +# Problem: pkgx SQLite conflicts with iOS builds +# Error: Linker errors about libsqlite3.dylib +# Solution: The build script automatically handles this by unsetting problematic +# environment variables. If issues persist: +unset PKGX_DIR DYLD_LIBRARY_PATH LD_LIBRARY_PATH +./scripts/build-native.sh --platform ios + +# Problem: "pod install" fails +# Solution: Ensure Command Line Tools are installed +xcode-select --install +# Then reinstall CocoaPods dependencies +cd ios +pod deintegrate +pod install +``` + #### Capacitor Integration Issues ```bash # Problem: Plugin not found in Capacitor app diff --git a/COMMIT_MESSAGE.txt b/COMMIT_MESSAGE.txt index 94050b4..9c5f20d 100644 --- a/COMMIT_MESSAGE.txt +++ b/COMMIT_MESSAGE.txt @@ -1,76 +1,46 @@ -refactor(android): Complete plugin refactoring and safety fixes (Batches 0-7) +docs(building): update BUILDING.md with iOS prerequisites and clean-build script -Comprehensive refactoring to make DailyNotificationPlugin a thin adapter, -eliminate duplicated logic, remove unsafe operations, and harden security. +Updates BUILDING.md to reflect recent changes in build-native.sh, especially +the Xcode Command Line Tools prerequisite check and the clean-build script. -Batch 0 - Constants Centralization: -- Created DailyNotificationConstants.kt to eliminate magic numbers and duplicates -- Centralized: PERMISSION_REQUEST_CODE, channel constants, intent actions/extras, - SharedPreferences keys, WorkManager tags, notification IDs -- Replaced duplicates across Plugin, PermissionManager, ChannelManager, Scheduler +Problem: +- BUILDING.md didn't mention Xcode Command Line Tools prerequisite + (recently added to build-native.sh) +- clean-build.sh script exists but wasn't documented +- iOS build troubleshooting lacked Command Line Tools guidance -Batch 1 - Permission Flow Unification: -- Created PermissionStatus.kt data class for unified permission reporting -- Added PermissionManager.getPermissionStatus() as single source of truth -- Implemented PendingPermissionRequest tracking for reliable resume resolution -- Replaced method-name-based resume logic with token-based tracking -- Plugin now delegates all permission checks to PermissionManager +Changes: +- Add Xcode Command Line Tools to Prerequisites section + - Document installation command (xcode-select --install) + - Include verification steps (xcode-select -p, xcodebuild -version) + - Note that build script automatically checks for these tools + - Explain that sqlite3 is part of Command Line Tools -Batch 2 - Notification Status Checker Hardening: -- Modified NotificationStatusChecker to always check OS-level notification - enablement via NotificationManagerCompat.areNotificationsEnabled() -- Added getReadinessReport() method providing comprehensive status with issues - and actionable guidance -- Plugin checkStatus() now uses readiness report +- Document clean-build.sh script in Build Scripts section + - Basic usage: ./scripts/clean-build.sh + - All options: --all, --clean-gradle-cache, --clean-derived-data, + --reinstall-node + - Explain when to use clean builds -Batch 3 - Cancel Semantics Safety: -- Removed unsafe brute-force cancellation loop (was trying request codes 0-100) -- Cancellation now only targets alarms proven to exist in database -- Prevents accidental cancellation of other alarms and false confidence +- Enhance iOS Native Build Process section + - Add prerequisite note about Command Line Tools + - Include troubleshooting commands for pod install issues + - Reference prerequisites section for details -Batch 4 - Legacy Scheduler Removal: -- Removed unused legacy scheduleExactAlarm() method (48 lines) -- All scheduling now goes through modern paths: - 1. exactAlarmManager.scheduleAlarm() (if available) - 2. pendingIntentManager.scheduleExactAlarm() (modern path) - 3. pendingIntentManager.scheduleWindowedAlarm() (fallback) +- Add comprehensive troubleshooting sections + - Clean Build section at start of Troubleshooting + - Recommends clean-build as first step for many issues + - Lists when to use clean builds + - iOS Build Issues section + - Command Line Tools configuration errors + - SQLite/linker issues and pkgx conflicts + - CocoaPods installation problems + - All with clear solutions and commands -Batch 5 - Input Contract Tightening: -- Enforced single input shape for updateStarredPlans: { planIds: string[] } -- Added validation: rejects non-array, non-string elements, empty strings -- Legacy support: single string normalized to array (with warning) -- Clear error messages for contract violations - -Batch 6 - Token Storage Security: -- Added explicit opt-in for JWT token persistence (persistToken: true) -- Default behavior: tokens NOT persisted (secure default) -- Security warnings logged when persistence is enabled -- Documents unencrypted storage risk - -Batch 7 - Plugin Thinning: -- Moved getExactAlarmStatus() to PermissionManager.getExactAlarmStatus() -- Moved canRequestExactAlarmPermission() to PermissionManager -- Removed direct AlarmManager access in cancelAllNotifications() -- Delegated scheduleUserNotification/scheduleDualNotification permission - handling to PermissionManager.requestExactAlarmPermission() -- Removed unused imports: AlarmManager, PendingIntent, PowerManager, - NotificationManagerCompat - -Result: -- Plugin is now a thin adapter delegating to services -- No duplicated permission logic -- No unsafe cancellation operations -- No legacy scheduler paths -- Secure token storage defaults -- Clear input contracts -- Comprehensive status reporting +The documentation now accurately reflects: +- Xcode Command Line Tools as required iOS prerequisite +- clean-build.sh as available build tool +- Complete iOS troubleshooting workflow Files modified: -- DailyNotificationConstants.kt (new) -- PermissionStatus.kt (new) -- DailyNotificationPlugin.kt (thinned, ~500 lines refactored) -- PermissionManager.java (enhanced with status methods) -- NotificationStatusChecker.java (hardened) -- DailyNotificationScheduler.java (legacy removed) - -Refs: Cursor directive Batches 0-7 +- BUILDING.md diff --git a/android/gradle.properties b/android/gradle.properties index 3a1b633..2a26d1c 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -22,7 +22,32 @@ org.gradle.caching=true org.gradle.parallel=true # Increase memory for Gradle daemon -org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m +# Java 17+ requires --add-opens flags for KAPT to access internal compiler classes +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m \ + --add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + +# Kotlin compiler daemon JVM arguments (required for KAPT with Java 17+) +# The Kotlin daemon runs separately and needs the same --add-opens flags +kotlin.daemon.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m \ + --add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED # Enable configuration cache org.gradle.configuration-cache=true diff --git a/ios/DailyNotificationPlugin.podspec b/ios/DailyNotificationPlugin.podspec index 0b374ae..fd528d2 100644 --- a/ios/DailyNotificationPlugin.podspec +++ b/ios/DailyNotificationPlugin.podspec @@ -11,7 +11,13 @@ Pod::Spec.new do |s| s.dependency 'Capacitor', '>= 5.0.0' s.dependency 'CapacitorCordova', '>= 5.0.0' s.swift_version = '5.1' - s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) COCOAPODS=1' } + # Explicitly link against system SQLite library to avoid conflicts with + # macOS SQLite libraries (e.g., from pkgx or other package managers that + # may set DYLD_LIBRARY_PATH or similar environment variables) + s.xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) COCOAPODS=1', + 'OTHER_LDFLAGS' => '$(inherited) -lsqlite3' + } s.deprecated = false # Set to false so Capacitor can discover the plugin # Capacitor iOS does not scan static frameworks for plugin discovery diff --git a/ios/DailyNotificationPlugin.xcworkspace/xcuserdata/cloud.xcuserdatad/UserInterfaceState.xcuserstate b/ios/DailyNotificationPlugin.xcworkspace/xcuserdata/cloud.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 51b5014..0000000 Binary files a/ios/DailyNotificationPlugin.xcworkspace/xcuserdata/cloud.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/ios/Plugin/DailyNotificationPlugin.swift b/ios/Plugin/DailyNotificationPlugin.swift index 2af6473..dda9fd1 100644 --- a/ios/Plugin/DailyNotificationPlugin.swift +++ b/ios/Plugin/DailyNotificationPlugin.swift @@ -7,10 +7,12 @@ // import Foundation +import UIKit import Capacitor import UserNotifications import BackgroundTasks import CoreData +import ObjectiveC /** * iOS implementation of Daily Notification Plugin @@ -80,8 +82,50 @@ public class DailyNotificationPlugin: CAPPlugin { object: nil ) + NSLog("DNP-ROLLOVER: Observer registered for DailyNotificationDelivered notification") + print("DNP-ROLLOVER: Observer registered for DailyNotificationDelivered notification") + + // Register for app becoming active to check for missed rollovers + NotificationCenter.default.addObserver( + self, + selector: #selector(handleAppBecameActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + + NSLog("DNP-ROLLOVER: Observer registered for app becoming active") + print("DNP-ROLLOVER: Observer registered for app becoming active") + NSLog("DNP-DEBUG: DailyNotificationPlugin.load() completed - initialization done") print("DNP-PLUGIN: Daily Notification Plugin loaded on iOS") + + // Debug: Log all available @objc methods for Capacitor discovery + let methods = getObjCMethods() + NSLog("DNP-DEBUG: Available @objc methods: \(methods.joined(separator: ", "))") + print("DNP-DEBUG: Available @objc methods: \(methods.joined(separator: ", "))") + } + + /** + * Debug helper: Get all @objc methods for this class + */ + private func getObjCMethods() -> [String] { + var methods: [String] = [] + var methodCount: UInt32 = 0 + let methodList = class_copyMethodList(type(of: self), &methodCount) + + for i in 0.. RolloverRecoveryResult { + NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_CHECK_START") + print("DNP-ROLLOVER: MISSED_ROLLOVER_CHECK_START") + + let currentTime = Int64(Date().timeIntervalSince1970 * 1000) + let currentTimeStr = formatTime(currentTime) + + // Step 1: Get all notifications from storage + let allNotifications: [NotificationContent] + do { + allNotifications = storage.getAllNotifications() + } catch { + // Non-fatal: Log error and return empty result + NSLog("\(Self.TAG): Error getting notifications from storage (non-fatal): \(error.localizedDescription)") + return RolloverRecoveryResult(processedCount: 0, failedCount: 0, totalChecked: 0) + } + + // Step 2: Get pending notifications from system + let pendingRequests: [UNNotificationRequest] + do { + pendingRequests = try await notificationCenter.pendingNotificationRequests() + } catch { + // Non-fatal: Log error and continue with empty pending list + NSLog("\(Self.TAG): Error getting pending notifications (non-fatal): \(error.localizedDescription)") + return RolloverRecoveryResult(processedCount: 0, failedCount: 0, totalChecked: allNotifications.count) + } + + let pendingIds = Set(pendingRequests.map { $0.identifier }) + + // Step 3: Find notifications that should have rolled over + var missedRollovers: [NotificationContent] = [] + + for notification in allNotifications { + // Check if notification should have fired (scheduledTime < currentTime) + if notification.scheduledTime >= currentTime { + continue // Future notification, skip + } + + // Check if rollover was already processed + // Only skip if rollover was processed AND next notification exists + // This handles cases where rollover was attempted but failed + let lastRolloverTime = await storage.getLastRolloverTime(for: notification.id) + + // Calculate next scheduled time first to check if it exists + var nextScheduledTime = scheduler.calculateNextScheduledTime(notification.scheduledTime) + + // If next scheduled time is in the past, keep calculating forward until we get a future time + // This handles cases where the notification fired more than 2 minutes ago + while nextScheduledTime < currentTime { + let nextTimeStr = formatTime(nextScheduledTime) + NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_NEXT_IN_PAST id=\(notification.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward") + print("DNP-ROLLOVER: MISSED_ROLLOVER_NEXT_IN_PAST id=\(notification.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward") + nextScheduledTime = scheduler.calculateNextScheduledTime(nextScheduledTime) + } + + // Check if next notification actually exists + let toleranceMs: Int64 = 60 * 1000 // 1 minute tolerance + var nextNotificationExists = false + + // Quick check in storage (exclude original) + for existing in allNotifications { + if existing.id == notification.id { + continue + } + if abs(existing.scheduledTime - nextScheduledTime) <= toleranceMs { + nextNotificationExists = true + break + } + } + + // Quick check in pending + if !nextNotificationExists { + for pending in pendingRequests { + if pending.identifier == notification.id { + continue + } + if let trigger = pending.trigger as? UNCalendarNotificationTrigger, + let nextDate = trigger.nextTriggerDate() { + let pendingTime = Int64(nextDate.timeIntervalSince1970 * 1000) + if abs(pendingTime - nextScheduledTime) <= toleranceMs { + nextNotificationExists = true + break + } + } + } + } + + // If rollover was processed AND next notification exists, skip + // Otherwise, process it (either rollover wasn't attempted, or it failed) + if let lastTime = lastRolloverTime, lastTime >= notification.scheduledTime, nextNotificationExists { + let lastTimeStr = formatTime(lastTime) + NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_SKIP id=\(notification.id) already_processed last_rollover=\(lastTimeStr) next_exists=true") + print("DNP-ROLLOVER: MISSED_ROLLOVER_SKIP id=\(notification.id) already_processed last_rollover=\(lastTimeStr) next_exists=true") + continue // Already processed and next notification exists + } + + // If rollover was attempted but next notification doesn't exist, log and continue processing + if let lastTime = lastRolloverTime, lastTime >= notification.scheduledTime, !nextNotificationExists { + let lastTimeStr = formatTime(lastTime) + NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_RETRY id=\(notification.id) rollover_attempted=\(lastTimeStr) but_next_missing, will_retry") + print("DNP-ROLLOVER: MISSED_ROLLOVER_RETRY id=\(notification.id) rollover_attempted=\(lastTimeStr) but_next_missing, will_retry") + // Continue to process - rollover was attempted but failed + } + + // Re-check if next notification exists (we already calculated nextScheduledTime above) + // This is the final check before adding to missed rollovers list + if !nextNotificationExists { + let nextTimeStr = formatTime(nextScheduledTime) + NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_DETECTED id=\(notification.id) scheduled_time=\(formatTime(notification.scheduledTime)) next_time=\(nextTimeStr)") + print("DNP-ROLLOVER: MISSED_ROLLOVER_DETECTED id=\(notification.id) scheduled_time=\(formatTime(notification.scheduledTime)) next_time=\(nextTimeStr)") + missedRollovers.append(notification) + } + } + + NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_FOUND count=\(missedRollovers.count) current_time=\(currentTimeStr)") + print("DNP-ROLLOVER: MISSED_ROLLOVER_FOUND count=\(missedRollovers.count) current_time=\(currentTimeStr)") + + // Step 4: Process missed rollovers + var processedCount = 0 + var failedCount = 0 + + for notification in missedRollovers { + let scheduledTimeStr = formatTime(notification.scheduledTime) + NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_PROCESS id=\(notification.id) scheduled_time=\(scheduledTimeStr)") + print("DNP-ROLLOVER: MISSED_ROLLOVER_PROCESS id=\(notification.id) scheduled_time=\(scheduledTimeStr)") + + // Schedule next notification using existing rollover logic + // Note: fetcher parameter is unused - scheduler uses fetchScheduler instead + let scheduled = await scheduler.scheduleNextNotification( + notification, + storage: storage, + fetcher: nil // Unused - fetchScheduler handles prefetch scheduling + ) + + if scheduled { + processedCount += 1 + NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_SUCCESS id=\(notification.id)") + print("DNP-ROLLOVER: MISSED_ROLLOVER_SUCCESS id=\(notification.id)") + } else { + failedCount += 1 + NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_FAILED id=\(notification.id)") + print("DNP-ROLLOVER: MISSED_ROLLOVER_FAILED id=\(notification.id)") + } + } + + NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_COMPLETE processed=\(processedCount) failed=\(failedCount) total_checked=\(allNotifications.count)") + print("DNP-ROLLOVER: MISSED_ROLLOVER_COMPLETE processed=\(processedCount) failed=\(failedCount) total_checked=\(allNotifications.count)") + + return RolloverRecoveryResult( + processedCount: processedCount, + failedCount: failedCount, + totalChecked: allNotifications.count + ) + } + /** * Format time for logging * @@ -1132,6 +1365,15 @@ struct VerificationResult { let missingIds: [String] } +/** + * Rollover recovery result + */ +struct RolloverRecoveryResult { + let processedCount: Int + let failedCount: Int + let totalChecked: Int +} + /** * Reactivation errors */ diff --git a/ios/Plugin/DailyNotificationScheduler.swift b/ios/Plugin/DailyNotificationScheduler.swift index 7658fef..b8633d3 100644 --- a/ios/Plugin/DailyNotificationScheduler.swift +++ b/ios/Plugin/DailyNotificationScheduler.swift @@ -389,6 +389,10 @@ class DailyNotificationScheduler { * * @param currentScheduledTime Current scheduled time in milliseconds * @return Next scheduled time in milliseconds (24 hours later) + * + * TESTING: To test with shorter intervals (e.g., 2 minutes), change: + * - Line ~404: `.hour, value: 24` → `.minute, value: 2` + * - Line ~407: `(24 * 60 * 60 * 1000)` → `(2 * 60 * 1000)` */ func calculateNextScheduledTime(_ currentScheduledTime: Int64) -> Int64 { let calendar = Calendar.current @@ -396,8 +400,10 @@ class DailyNotificationScheduler { let currentTimeStr = formatTime(currentScheduledTime) // Add 24 hours (handles DST transitions automatically) + // TESTING: Change `.hour, value: 24` to `.minute, value: 2` for 2-minute testing guard let nextDate = calendar.date(byAdding: .hour, value: 24, to: currentDate) else { // Fallback to simple 24-hour addition if calendar calculation fails + // TESTING: Change `(24 * 60 * 60 * 1000)` to `(2 * 60 * 1000)` for 2-minute testing let fallbackTime = currentScheduledTime + (24 * 60 * 60 * 1000) let fallbackTimeStr = formatTime(fallbackTime) NSLog("DNP-ROLLOVER: DST_CALC_FAILED current=\(currentTimeStr) using_fallback=\(fallbackTimeStr)") @@ -456,6 +462,7 @@ class DailyNotificationScheduler { let lastRolloverTime = await storage.getLastRolloverTime(for: content.id) // If rollover was processed recently (< 1 hour ago), skip + // TESTING: Change `(60 * 60 * 1000)` to `(60 * 1000)` for 1-minute threshold when testing with 2-minute intervals if let lastTime = lastRolloverTime, (currentTime - lastTime) < (60 * 60 * 1000) { let lastTimeStr = formatTime(lastTime) @@ -467,7 +474,17 @@ class DailyNotificationScheduler { } // Calculate next occurrence using DST-safe calculation - let nextScheduledTime = calculateNextScheduledTime(content.scheduledTime) + var nextScheduledTime = calculateNextScheduledTime(content.scheduledTime) + + // If next scheduled time is in the past, keep calculating forward until we get a future time + // This handles cases where the notification fired more than 2 minutes ago + while nextScheduledTime < currentTime { + let nextTimeStr = formatTime(nextScheduledTime) + NSLog("DNP-ROLLOVER: NEXT_IN_PAST id=\(content.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward") + print("DNP-ROLLOVER: NEXT_IN_PAST id=\(content.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward") + nextScheduledTime = calculateNextScheduledTime(nextScheduledTime) + } + let nextScheduledTimeStr = formatTime(nextScheduledTime) let hoursUntilNext = Double(nextScheduledTime - currentTime) / 1000.0 / 60.0 / 60.0 @@ -538,6 +555,12 @@ class DailyNotificationScheduler { let scheduled = await scheduleNotification(nextContent) if scheduled { + // Save notification content to storage so it can be retrieved when rollover fires + // This is critical: without saving, processRollover won't find the content + storage?.saveNotificationContent(nextContent) + NSLog("DNP-ROLLOVER: SAVED id=\(content.id) next_id=\(nextId) content saved to storage") + print("DNP-ROLLOVER: SAVED id=\(content.id) next_id=\(nextId) content saved to storage") + // Verify the notification was actually scheduled let pendingCount = await getPendingNotificationCount() let isScheduled = await isNotificationScheduled(id: nextId) diff --git a/ios/Podfile.lock b/ios/Podfile.lock deleted file mode 100644 index 3ad50e5..0000000 --- a/ios/Podfile.lock +++ /dev/null @@ -1,29 +0,0 @@ -PODS: - - Capacitor (6.2.1): - - CapacitorCordova - - CapacitorCordova (6.2.1) - - DailyNotificationPlugin (1.0.0): - - Capacitor (>= 5.0.0) - - CapacitorCordova (>= 5.0.0) - -DEPENDENCIES: - - "Capacitor (from `../node_modules/@capacitor/ios`)" - - "CapacitorCordova (from `../node_modules/@capacitor/ios`)" - - DailyNotificationPlugin (from `.`) - -EXTERNAL SOURCES: - Capacitor: - :path: "../node_modules/@capacitor/ios" - CapacitorCordova: - :path: "../node_modules/@capacitor/ios" - DailyNotificationPlugin: - :path: "." - -SPEC CHECKSUMS: - Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf - CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff - DailyNotificationPlugin: bb72fde9eab3704a4e70af3c868a789da0650ddc - -PODFILE CHECKSUM: ac8c229d24347f6f83e67e6b95458e0b81e68f7c - -COCOAPODS: 1.16.2 diff --git a/scripts/build-native.sh b/scripts/build-native.sh index 7c598b7..66503ff 100755 --- a/scripts/build-native.sh +++ b/scripts/build-native.sh @@ -25,22 +25,103 @@ log_error() { # Validation functions check_command() { if ! command -v $1 &> /dev/null; then + # Try rbenv shims for pod command + if [ "$1" = "pod" ] && [ -f "$HOME/.rbenv/shims/pod" ]; then + log_info "Found pod in rbenv shims" + return 0 + fi log_error "$1 is not installed. Please install it first." exit 1 fi } -check_environment() { - # Check for required tools - check_command "node" - check_command "npm" - check_command "java" - - # Check for Gradle Wrapper instead of system gradle - if [ ! -f "android/gradlew" ]; then - log_error "Gradle wrapper not found at android/gradlew" +# Get pod command (handles rbenv) +get_pod_command() { + if command -v pod &> /dev/null; then + echo "pod" + elif [ -f "$HOME/.rbenv/shims/pod" ]; then + echo "$HOME/.rbenv/shims/pod" + else + log_error "CocoaPods (pod) not found. Please install CocoaPods first." exit 1 fi +} + +check_sqlite_conflicts() { + local PLATFORM=$1 + + if [ "$PLATFORM" != "ios" ] && [ "$PLATFORM" != "all" ]; then + return 0 + fi + + # Check for pkgx SQLite that might interfere with iOS builds + if [ -d "$HOME/.pkgx" ]; then + # Look for pkgx SQLite installations + PKGX_SQLITE=$(find "$HOME/.pkgx" -name "libsqlite3*.dylib" 2>/dev/null | head -1) + + if [ -n "$PKGX_SQLITE" ]; then + log_warn "⚠️ pkgx SQLite detected: $PKGX_SQLITE" + log_warn " This may cause iOS build failures (linking macOS dylib for iOS simulator)" + log_warn " The build script will unset problematic environment variables" + + # Check if library paths point to pkgx + if [ -n "$DYLD_LIBRARY_PATH" ] && echo "$DYLD_LIBRARY_PATH" | grep -q "\.pkgx"; then + log_warn " DYLD_LIBRARY_PATH contains pkgx paths - will be unset for build" + fi + if [ -n "$LD_LIBRARY_PATH" ] && echo "$LD_LIBRARY_PATH" | grep -q "\.pkgx"; then + log_warn " LD_LIBRARY_PATH contains pkgx paths - will be unset for build" + fi + fi + fi + + # Check for system SQLite (from Command Line Tools) + if command -v sqlite3 &> /dev/null; then + SQLITE_PATH=$(which sqlite3) + if [ "$SQLITE_PATH" = "/usr/bin/sqlite3" ]; then + log_info "✅ System SQLite found (from Command Line Tools): $SQLITE_PATH" + else + log_warn "⚠️ SQLite found at non-standard location: $SQLITE_PATH" + log_warn " Ensure this is compatible with iOS builds" + fi + else + log_warn "⚠️ sqlite3 command not found - Command Line Tools may not be installed" + log_warn " Install with: xcode-select --install" + fi +} + +check_command_line_tools() { + if [ "$(uname)" != "Darwin" ]; then + return 0 + fi + + # Check if xcode-select points to a valid developer directory + if ! xcode-select -p &> /dev/null; then + log_error "Xcode Command Line Tools not configured" + log_error " Run: xcode-select --install" + exit 1 + fi + + # Check if xcodebuild is available + if ! command -v xcodebuild &> /dev/null; then + log_error "xcodebuild not found - Command Line Tools may be incomplete" + log_error " Run: xcode-select --install" + exit 1 + fi + + # Verify sqlite3 is available (part of Command Line Tools) + if ! command -v sqlite3 &> /dev/null; then + log_warn "⚠️ sqlite3 not found - Command Line Tools may be incomplete" + log_warn " This may cause iOS build issues" + log_warn " Run: xcode-select --install" + fi +} + +check_environment() { + local PLATFORM=$1 + + # Check for required tools (always needed) + check_command "node" + check_command "npm" # Check Node.js version NODE_VERSION=$(node -v | cut -d. -f1 | tr -d 'v') @@ -49,17 +130,40 @@ check_environment() { exit 1 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 - log_error "Java version 11 or higher is required" - exit 1 - fi + # Platform-specific checks + if [ "$PLATFORM" = "android" ] || [ "$PLATFORM" = "all" ]; then + check_command "java" + + # Check for Gradle Wrapper instead of system gradle + if [ ! -f "android/gradlew" ]; then + log_error "Gradle wrapper not found at android/gradlew" + exit 1 + fi - # Check for Android SDK - if [ -z "$ANDROID_HOME" ]; then - log_error "ANDROID_HOME environment variable is not set" - exit 1 + # Check Java version + JAVA_VERSION=$(java -version 2>&1 | head -n 1 | cut -d'"' -f2 | cut -d. -f1) + if [ "$JAVA_VERSION" -lt 11 ]; then + log_error "Java version 11 or higher is required" + exit 1 + fi + + # Check for Android SDK + if [ -z "$ANDROID_HOME" ]; then + log_error "ANDROID_HOME environment variable is not set" + exit 1 + fi + fi + + if [ "$PLATFORM" = "ios" ] || [ "$PLATFORM" = "all" ]; then + # Check Command Line Tools + check_command_line_tools + + # Check for SQLite conflicts + check_sqlite_conflicts "$PLATFORM" + + # iOS-specific checks are done in build_ios() function + # to avoid failing if iOS tools aren't available when building Android only + : fi } @@ -76,30 +180,8 @@ build_typescript() { build_plugin_for_test_app() { log_info "Building plugin for test app..." - # Build the plugin AAR - log_info "Building plugin AAR..." - cd android || exit 1 - if ! ./gradlew :plugin:clean :plugin:assembleDebug; then - log_error "Plugin build failed" - exit 1 - fi - - AAR_FILE="plugin/build/outputs/aar/plugin-debug.aar" - if [ ! -f "$AAR_FILE" ]; then - log_error "AAR file not found at $AAR_FILE" - exit 1 - fi - log_info "Plugin AAR built: $AAR_FILE" - - # Remove any stale AAR from test app's libs directory - if [ -f "../test-apps/daily-notification-test/android/app/libs/plugin-debug.aar" ]; then - log_info "Removing stale AAR from test app libs..." - rm "../test-apps/daily-notification-test/android/app/libs/plugin-debug.aar" - fi - - cd .. - - # Ensure symlink is in place + # Ensure symlink is in place FIRST (before building) + # The test app expects the plugin at node_modules/@timesafari/daily-notification-plugin SYMLINK="test-apps/daily-notification-test/node_modules/@timesafari/daily-notification-plugin" if [ ! -L "$SYMLINK" ] || [ ! -e "$SYMLINK" ]; then log_info "Creating symlink to plugin..." @@ -108,27 +190,213 @@ build_plugin_for_test_app() { ln -sf "../../../" "$SYMLINK" fi - # Build test app - log_info "Building test app..." - cd test-apps/daily-notification-test/android || exit 1 + # Navigate to test app directory + cd test-apps/daily-notification-test || exit 1 - if ! ./gradlew clean assembleDebug; then - log_error "Test app build failed" + # Sync Capacitor Android (creates necessary project structure) + log_info "Syncing Capacitor Android..." + if ! npm run cap:sync:android; then + log_error "Capacitor Android sync failed" exit 1 fi + # Navigate to android directory + cd android || exit 1 + + # Fix capacitor.build.gradle if it references missing cordova.variables.gradle + # This file is auto-generated by Capacitor and may reference files that don't exist + if [ -f "app/capacitor.build.gradle" ]; then + if grep -q "^apply from: \"../capacitor-cordova-android-plugins/cordova.variables.gradle\"" "app/capacitor.build.gradle"; then + # Check if the referenced file actually exists + if [ ! -f "capacitor-cordova-android-plugins/cordova.variables.gradle" ]; then + log_info "🔧 Applying fix to capacitor.build.gradle..." + log_info " Reason: Referenced cordova.variables.gradle doesn't exist" + log_info " Fix: Commenting out problematic 'apply from' line" + + # Apply the fix by commenting out the problematic line + # Handle macOS vs Linux sed differences + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS sed requires empty string after -i + # Use a simpler approach: just comment out the line + sed -i '' 's|^apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"|// &|' "app/capacitor.build.gradle" + else + # Linux sed + sed -i 's|^apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"|// &|' "app/capacitor.build.gradle" + fi + + log_info "✅ Fix applied successfully" + log_warn "⚠️ Note: This fix will be lost if you run 'npx cap sync' or 'npx cap update'" + fi + fi + fi + + # Build the plugin and test app from within the test app's Android project context + # This is necessary because Capacitor Android is only available as a project dependency + # The plugin will be built automatically as a dependency of the app + log_info "Building plugin and test app from test app context..." + + # Build test app (this will automatically build the plugin as a dependency) + if ! ./gradlew clean assembleDebug; then + log_error "Build failed" + exit 1 + fi + + # Verify plugin was built (optional check) + PLUGIN_AAR="$(find . -name "*.aar" -path "*/timesafari-daily-notification-plugin/*" 2>/dev/null | head -1)" + if [ -n "$PLUGIN_AAR" ]; then + log_info "Plugin AAR built: $PLUGIN_AAR" + fi + + # Verify APK was built APK_FILE="app/build/outputs/apk/debug/app-debug.apk" if [ ! -f "$APK_FILE" ]; then log_error "APK file not found at $APK_FILE" exit 1 fi - log_info "Test app build successful: $APK_FILE" + log_info "Build successful: $APK_FILE" log_info "Install with: adb install -r $APK_FILE" cd ../../.. } +build_plugin_for_test_app_ios() { + log_info "Building iOS test app..." + + # Fix pkgx interference with SQLite linking + # pkgx installs SQLite built for macOS, which causes linker errors when building for iOS simulator + if [ -n "$PKGX_DIR" ] || [ -d "$HOME/.pkgx" ]; then + log_warn "⚠️ pkgx detected - fixing SQLite linking conflicts..." + + # Check for pkgx SQLite specifically + PKGX_SQLITE=$(find "$HOME/.pkgx" -name "libsqlite3*.dylib" 2>/dev/null | head -1) + if [ -n "$PKGX_SQLITE" ]; then + log_warn " Found pkgx SQLite: $PKGX_SQLITE" + log_warn " This is built for macOS and will cause iOS simulator build failures" + fi + + log_warn " Temporarily unsetting PKGX_DIR and library paths for build..." + unset PKGX_DIR + unset DYLD_LIBRARY_PATH + unset LD_LIBRARY_PATH + + # Also unset any pkgx-related paths that might be in PATH + export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "\.pkgx" | tr '\n' ':' | sed 's/:$//') + fi + + # Ensure symlink is in place FIRST (before building) + # The test app expects the plugin at node_modules/@timesafari/daily-notification-plugin + SYMLINK="test-apps/daily-notification-test/node_modules/@timesafari/daily-notification-plugin" + if [ ! -L "$SYMLINK" ] || [ ! -e "$SYMLINK" ]; then + log_info "Creating symlink to plugin..." + mkdir -p "$(dirname "$SYMLINK")" + rm -f "$SYMLINK" + ln -sf "../../../" "$SYMLINK" + fi + + # Navigate to test app directory + cd test-apps/daily-notification-test || exit 1 + + # Install dependencies if node_modules doesn't exist or is missing key packages + if [ ! -d "node_modules" ] || [ ! -f "node_modules/.bin/run-p" ]; then + log_info "Installing test app dependencies..." + if ! npm install; then + log_error "Failed to install test app dependencies" + exit 1 + fi + fi + + # Build Vue app + log_info "Building Vue app..." + if ! npm run build; then + log_error "Vue app build failed" + exit 1 + fi + + # Sync Capacitor iOS (handles Podfile fixes and pod install) + log_info "Syncing Capacitor iOS..." + if ! npm run cap:sync:ios; then + log_error "Capacitor iOS sync failed" + exit 1 + fi + + # Build with xcodebuild + cd ios/App || exit 1 + + # Find workspace + if [ -d "App.xcworkspace" ]; then + WORKSPACE="App.xcworkspace" + SCHEME="App" + elif [ -d "App.xcodeproj" ]; then + PROJECT="App.xcodeproj" + SCHEME="App" + else + log_error "No Xcode workspace or project found in ios/App" + log_info "Expected: App.xcworkspace or App.xcodeproj" + exit 1 + fi + + # Auto-detect available iPhone simulator + log_info "Detecting available iPhone simulator..." + SIMULATOR_LINE=$(xcrun simctl list devices available 2>&1 | grep -i "iPhone" | head -1) + + if [ -n "$SIMULATOR_LINE" ]; then + # Extract device ID (UUID in parentheses) + SIMULATOR_ID=$(echo "$SIMULATOR_LINE" | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/') + # Extract device name (everything before the first parenthesis) + SIMULATOR_NAME=$(echo "$SIMULATOR_LINE" | sed -E 's/^[[:space:]]*([^(]+).*/\1/' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + if [ -n "$SIMULATOR_ID" ] && [ "$SIMULATOR_ID" != "Shutdown" ] && [ "$SIMULATOR_ID" != "Booted" ]; then + DESTINATION="platform=iOS Simulator,id=$SIMULATOR_ID" + log_info "Building for iOS Simulator ($SIMULATOR_NAME, ID: $SIMULATOR_ID)..." + elif [ -n "$SIMULATOR_NAME" ]; then + DESTINATION="platform=iOS Simulator,name=$SIMULATOR_NAME" + log_info "Building for iOS Simulator ($SIMULATOR_NAME)..." + else + DESTINATION="platform=iOS Simulator,name=iPhone 14" + log_warn "Using default simulator destination: iPhone 14" + fi + else + DESTINATION="platform=iOS Simulator,name=iPhone 14" + log_warn "No iPhone simulators found, using default: iPhone 14" + fi + + # Build for simulator + log_info "Building iOS app for simulator..." + if [ -n "$WORKSPACE" ]; then + if ! xcodebuild build \ + -workspace "$WORKSPACE" \ + -scheme "$SCHEME" \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination "$DESTINATION" \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO; then + log_error "iOS build failed" + exit 1 + fi + else + if ! xcodebuild build \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination "$DESTINATION" \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO; then + log_error "iOS build failed" + exit 1 + fi + fi + + log_info "iOS build successful!" + log_info "App built in DerivedData. To run: xcrun simctl boot && xcrun simctl install booted " + + cd ../../.. +} + build_android() { log_info "Building Android..." @@ -237,6 +505,75 @@ build_android() { cd .. } +build_ios() { + log_info "Building iOS..." + + # Check iOS-specific requirements + check_command "xcodebuild" + check_command "pod" + + # Check for Xcode + if ! xcodebuild -version &> /dev/null; then + log_error "Xcode is not installed or not properly configured" + exit 1 + fi + + # Check for SQLite conflicts (already done in check_environment, but remind here) + if [ -d "$HOME/.pkgx" ]; then + PKGX_SQLITE=$(find "$HOME/.pkgx" -name "libsqlite3*.dylib" 2>/dev/null | head -1) + if [ -n "$PKGX_SQLITE" ]; then + log_warn "⚠️ pkgx SQLite detected - build script will handle this automatically" + log_warn " If you still see linker errors, manually run:" + log_warn " unset PKGX_DIR DYLD_LIBRARY_PATH LD_LIBRARY_PATH && ./scripts/build-native.sh --platform ios" + fi + fi + + # Detect project type + if [ -d "test-apps/daily-notification-test" ]; then + log_info "Detected test app. Building plugin and test app together..." + build_plugin_for_test_app_ios + return 0 + fi + + # For plugin-only builds, navigate to iOS directory + cd ios || exit 1 + + log_warn "This appears to be a plugin development project" + log_warn "iOS plugin source code has been built successfully" + log_warn "To test the plugin, use it in a Capacitor app instead" + + # Install CocoaPods dependencies if Podfile exists + if [ -f "Podfile" ]; then + log_info "Installing CocoaPods dependencies..." + POD_CMD=$(get_pod_command) + if ! $POD_CMD install; then + log_error "CocoaPods installation failed" + exit 1 + fi + fi + + # Build plugin framework (if workspace exists) + if [ -d "DailyNotificationPlugin.xcworkspace" ]; then + log_info "Building iOS plugin framework..." + if ! xcodebuild build \ + -workspace DailyNotificationPlugin.xcworkspace \ + -scheme DailyNotificationPlugin \ + -configuration Release \ + -sdk iphoneos \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO; then + log_error "iOS plugin build failed" + exit 1 + fi + log_info "iOS plugin build successful" + else + log_warn "No Xcode workspace found - plugin may need to be built in a host app" + fi + + cd .. +} + # Main build process main() { log_info "Starting build process..." @@ -256,22 +593,26 @@ main() { esac done - # Check environment - check_environment - - # Build TypeScript + # Build TypeScript (needed for all platforms) build_typescript + # Check environment (after parsing platform so we can check conditionally) + check_environment "$BUILD_PLATFORM" + # Build based on platform case $BUILD_PLATFORM in "android") build_android ;; + "ios") + build_ios + ;; "all") 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 ;; esac diff --git a/scripts/clean-build.sh b/scripts/clean-build.sh new file mode 100755 index 0000000..38430db --- /dev/null +++ b/scripts/clean-build.sh @@ -0,0 +1,271 @@ +#!/bin/bash + +# Exit on error +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 + +# Logging functions +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" +} + +# Parse command line arguments +REINSTALL_NODE_MODULES=false +CLEAN_GRADLE_CACHE=false +CLEAN_DERIVED_DATA=false + +while [[ $# -gt 0 ]]; do + case $1 in + --reinstall-node) + REINSTALL_NODE_MODULES=true + shift + ;; + --clean-gradle-cache) + CLEAN_GRADLE_CACHE=true + shift + ;; + --clean-derived-data) + CLEAN_DERIVED_DATA=true + shift + ;; + --all) + REINSTALL_NODE_MODULES=true + CLEAN_GRADLE_CACHE=true + CLEAN_DERIVED_DATA=true + shift + ;; + *) + log_error "Unknown option: $1" + echo "Usage: $0 [--reinstall-node] [--clean-gradle-cache] [--clean-derived-data] [--all]" + exit 1 + ;; + esac +done + +log_info "Starting clean build process..." +log_warn "This will remove all build artifacts and caches" + +# Step 1: Clean TypeScript build output +log_step "1. Cleaning TypeScript build output..." +npm run clean 2>/dev/null || log_warn "npm run clean failed (may not exist)" +rm -rf dist/ +log_info "✓ TypeScript build output cleaned" + +# Step 2: Clean iOS plugin build artifacts +log_step "2. Cleaning iOS plugin build artifacts..." +if [ -d "ios" ]; then + cd ios + + # Remove Pods and Podfile.lock + if [ -d "Pods" ]; then + rm -rf Pods/ + log_info " ✓ Removed Pods/" + fi + if [ -f "Podfile.lock" ]; then + rm -f Podfile.lock + log_info " ✓ Removed Podfile.lock" + fi + + # Remove Xcode build artifacts + if [ -d "DailyNotificationPlugin.xcworkspace/xcuserdata" ]; then + rm -rf DailyNotificationPlugin.xcworkspace/xcuserdata/ + log_info " ✓ Removed workspace user data" + fi + if [ -d "DailyNotificationPlugin.xcodeproj/xcuserdata" ]; then + rm -rf DailyNotificationPlugin.xcodeproj/xcuserdata/ + log_info " ✓ Removed project user data" + fi + + # Remove build directories + find . -type d -name "build" -exec rm -rf {} + 2>/dev/null || true + find . -type d -name "DerivedData" -exec rm -rf {} + 2>/dev/null || true + + cd .. + log_info "✓ iOS plugin cleaned" +fi + +# Step 3: Clean Android plugin build artifacts +log_step "3. Cleaning Android plugin build artifacts..." +if [ -d "android" ]; then + cd android + + # Remove build directories + if [ -d "build" ]; then + rm -rf build/ + log_info " ✓ Removed build/" + fi + if [ -d "app/build" ]; then + rm -rf app/build/ + log_info " ✓ Removed app/build/" + fi + if [ -d ".gradle" ]; then + if [ "$CLEAN_GRADLE_CACHE" = true ]; then + rm -rf .gradle/ + log_info " ✓ Removed .gradle/ (cache cleaned)" + else + log_info " ℹ .gradle/ preserved (use --clean-gradle-cache to remove)" + fi + fi + + cd .. + log_info "✓ Android plugin cleaned" +fi + +# Step 4: Clean test app build artifacts +log_step "4. Cleaning test app build artifacts..." +if [ -d "test-apps/daily-notification-test" ]; then + cd test-apps/daily-notification-test + + # Remove symlink + if [ -L "node_modules/@timesafari/daily-notification-plugin" ]; then + rm -f node_modules/@timesafari/daily-notification-plugin + log_info " ✓ Removed plugin symlink" + fi + + # Remove node_modules (if requested) + if [ "$REINSTALL_NODE_MODULES" = true ]; then + if [ -d "node_modules" ]; then + # Use find to handle permission issues on macOS + find node_modules -delete 2>/dev/null || rm -rf node_modules/ 2>/dev/null || { + log_warn " ⚠ Some files in node_modules/ could not be deleted (permission issues)" + log_warn " ℹ You may need to manually remove node_modules/ or use sudo" + } + log_info " ✓ Removed node_modules/ (will reinstall)" + fi + fi + + # Remove build output + if [ -d "dist" ]; then + rm -rf dist/ + log_info " ✓ Removed dist/" + fi + + # Clean iOS test app + if [ -d "ios" ]; then + cd ios/App + + # Remove Pods and Podfile.lock + if [ -d "Pods" ]; then + rm -rf Pods/ + log_info " ✓ Removed iOS test app Pods/" + fi + if [ -f "Podfile.lock" ]; then + rm -f Podfile.lock + log_info " ✓ Removed iOS test app Podfile.lock" + fi + + # Remove Xcode build artifacts + if [ -d "App.xcworkspace/xcuserdata" ]; then + rm -rf App.xcworkspace/xcuserdata/ + fi + if [ -d "App.xcodeproj/xcuserdata" ]; then + rm -rf App.xcodeproj/xcuserdata/ + fi + + # Remove build directories + find . -type d -name "build" -exec rm -rf {} + 2>/dev/null || true + + cd ../.. + fi + + # Clean Android test app + if [ -d "android" ]; then + cd android + + # Remove build directories + if [ -d "build" ]; then + rm -rf build/ + log_info " ✓ Removed Android test app build/" + fi + if [ -d "app/build" ]; then + rm -rf app/build/ + log_info " ✓ Removed Android test app app/build/" + fi + if [ -d "capacitor-cordova-android-plugins/build" ]; then + rm -rf capacitor-cordova-android-plugins/build/ + log_info " ✓ Removed Android test app plugin build/" + fi + if [ -d ".gradle" ] && [ "$CLEAN_GRADLE_CACHE" = true ]; then + rm -rf .gradle/ + log_info " ✓ Removed Android test app .gradle/ (cache cleaned)" + fi + + cd .. + fi + + cd ../.. + log_info "✓ Test app cleaned" +fi + +# Step 5: Clean Xcode DerivedData (if requested) +if [ "$CLEAN_DERIVED_DATA" = true ]; then + log_step "5. Cleaning Xcode DerivedData..." + DERIVED_DATA_DIR="$HOME/Library/Developer/Xcode/DerivedData" + if [ -d "$DERIVED_DATA_DIR" ]; then + # Find and remove project-specific DerivedData + find "$DERIVED_DATA_DIR" -maxdepth 1 -type d \( -name "*DailyNotification*" -o -name "*daily-notification*" \) -exec rm -rf {} + 2>/dev/null || true + log_info "✓ Xcode DerivedData cleaned (project-specific)" + log_warn " ℹ To clean all DerivedData, manually delete: $DERIVED_DATA_DIR" + else + log_warn " ℹ DerivedData directory not found: $DERIVED_DATA_DIR" + fi +fi + +# Step 6: Reinstall node_modules (if requested) +if [ "$REINSTALL_NODE_MODULES" = true ]; then + log_step "6. Reinstalling node_modules..." + + # Root node_modules + if [ -d "node_modules" ]; then + find node_modules -delete 2>/dev/null || rm -rf node_modules/ 2>/dev/null || { + log_warn " ⚠ Some files in root node_modules/ could not be deleted (permission issues)" + log_warn " ℹ You may need to manually remove node_modules/ or use sudo" + } + fi + log_info " Installing root dependencies..." + npm install + + # Test app node_modules + if [ -d "test-apps/daily-notification-test" ]; then + cd test-apps/daily-notification-test + log_info " Installing test app dependencies..." + npm install + cd ../.. + fi + + log_info "✓ Dependencies reinstalled" +fi + +log_info "" +log_info "════════════════════════════════════════════════════════════" +log_info "Clean build complete! 🎉" +log_info "════════════════════════════════════════════════════════════" +log_info "" +log_info "Next steps:" +log_info " 1. Run: ./scripts/build-native.sh --platform all" +log_info " 2. Or run: ./scripts/build-native.sh --platform ios" +log_info " 3. Or run: ./scripts/build-native.sh --platform android" +log_info "" +log_info "Options used:" +[ "$REINSTALL_NODE_MODULES" = true ] && log_info " ✓ Reinstalled node_modules" +[ "$CLEAN_GRADLE_CACHE" = true ] && log_info " ✓ Cleaned Gradle cache" +[ "$CLEAN_DERIVED_DATA" = true ] && log_info " ✓ Cleaned Xcode DerivedData" +log_info "" diff --git a/test-apps/daily-notification-test/android/gradle.properties b/test-apps/daily-notification-test/android/gradle.properties index 2e87c52..93496c3 100644 --- a/test-apps/daily-notification-test/android/gradle.properties +++ b/test-apps/daily-notification-test/android/gradle.properties @@ -9,7 +9,32 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m +# Java 17+ requires --add-opens flags for KAPT to access internal compiler classes +org.gradle.jvmargs=-Xmx1536m \ + --add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + +# Kotlin compiler daemon JVM arguments (required for KAPT with Java 17+) +# The Kotlin daemon runs separately and needs the same --add-opens flags +kotlin.daemon.jvmargs=-Xmx1536m \ + --add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/test-apps/daily-notification-test/index.html b/test-apps/daily-notification-test/index.html index 9e5fc8f..8bdf2b9 100644 --- a/test-apps/daily-notification-test/index.html +++ b/test-apps/daily-notification-test/index.html @@ -1,13 +1,13 @@ - + Vite App - -
+ +
diff --git a/test-apps/daily-notification-test/ios/App/App/AppDelegate.swift b/test-apps/daily-notification-test/ios/App/App/AppDelegate.swift index c3cd83b..3ae4e14 100644 --- a/test-apps/daily-notification-test/ios/App/App/AppDelegate.swift +++ b/test-apps/daily-notification-test/ios/App/App/AppDelegate.swift @@ -1,12 +1,52 @@ import UIKit import Capacitor +import BackgroundTasks +import DailyNotificationPlugin +import ObjectiveC +import UserNotifications @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + NSLog("DNP-DEBUG: AppDelegate.application(_:didFinishLaunchingWithOptions:) called") + + // CRITICAL: Force-load the plugin framework before Capacitor initializes + // objc_getClassList may not include classes from frameworks that haven't been loaded yet + // Even though NSClassFromString can find the class, Capacitor's discovery uses objc_getClassList + // which only includes loaded classes. We need to ensure the framework is loaded. + NSLog("DNP-DEBUG: Force-loading DailyNotificationPlugin framework...") + _ = DailyNotificationPlugin.self // Force class load + NSLog("DNP-DEBUG: DailyNotificationPlugin class reference created - framework should be loaded") + + // Verify class is now in objc_getClassList + let classCount = objc_getClassList(nil, 0) + let classes = UnsafeMutablePointer.allocate(capacity: Int(classCount)) + defer { classes.deallocate() } + let releasingClasses = AutoreleasingUnsafeMutablePointer(classes) + let numClasses = objc_getClassList(releasingClasses, Int32(classCount)) + + var foundInClassList = false + for i in 0.. Void) { + NSLog("DNP-DEBUG: ✅ userNotificationCenter willPresent called!") + NSLog("DNP-DEBUG: Notification received in foreground: %@", notification.request.identifier) + NSLog("DNP-DEBUG: Notification title: %@", notification.request.content.title) + NSLog("DNP-DEBUG: Notification body: %@", notification.request.content.body) + + // Extract notification info from userInfo for rollover + let userInfo = notification.request.content.userInfo + if let notificationId = userInfo["notification_id"] as? String, + let scheduledTime = userInfo["scheduled_time"] as? Int64 { + + // Format scheduled time for logging + let scheduledDate = Date(timeIntervalSince1970: Double(scheduledTime) / 1000.0) + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + let scheduledTimeStr = formatter.string(from: scheduledDate) + + NSLog("DNP-ROLLOVER: APPDELGATE_DETECTED id=%@ scheduled_time=%@", notificationId, scheduledTimeStr) + NSLog("DNP-DEBUG: Posted rollover notification for id=%@", notificationId) + + // Post notification to trigger rollover (decoupled pattern) + NotificationCenter.default.post( + name: NSNotification.Name("DailyNotificationDelivered"), + object: nil, + userInfo: [ + "notification_id": notificationId, + "scheduled_time": scheduledTime + ] + ) + } else { + NSLog("DNP-ROLLOVER: APPDELGATE_MISSING_DATA id=%@ userInfo=%@", notification.request.identifier, userInfo) + } + + // Show notification with banner, sound, and badge + // Use .banner for iOS 14+, fallback to .alert for iOS 13 + if #available(iOS 14.0, *) { + completionHandler([.banner, .sound, .badge]) + } else { + completionHandler([.alert, .sound, .badge]) + } + + NSLog("DNP-DEBUG: ✅ Completion handler called with presentation options") + } + + /** + * Handle notification tap/interaction + */ + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + NSLog("DNP-DEBUG: Notification tapped: %@", response.notification.request.identifier) + NSLog("DNP-DEBUG: Action identifier: %@", response.actionIdentifier) + + // Handle notification tap if needed + completionHandler() } func applicationWillTerminate(_ application: UIApplication) { diff --git a/test-apps/daily-notification-test/package.json b/test-apps/daily-notification-test/package.json index 70ac1b8..9dcd23c 100644 --- a/test-apps/daily-notification-test/package.json +++ b/test-apps/daily-notification-test/package.json @@ -8,14 +8,14 @@ }, "scripts": { "dev": "vite", - "build": "run-p type-check \"build-only {@}\" --", + "build": "npx run-p type-check \"build-only {@}\" --", "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --build", "lint": "eslint . --fix", "cap:sync": "npx cap sync && node scripts/fix-capacitor-plugins.js", "cap:sync:android": "npx cap sync android && node scripts/fix-capacitor-plugins.js", - "cap:sync:ios": "npx cap copy ios && node scripts/fix-capacitor-plugins.js && cd ios/App && pod install && cd ../..", + "cap:sync:ios": "npx cap copy ios && node scripts/fix-capacitor-plugins.js && cd ios/App && ../../scripts/pod-install.sh && cd ../..", "postinstall": "node scripts/fix-capacitor-plugins.js" }, "dependencies": { diff --git a/test-apps/daily-notification-test/scripts/fix-capacitor-plugins.js b/test-apps/daily-notification-test/scripts/fix-capacitor-plugins.js index 28ffe17..956e0e4 100755 --- a/test-apps/daily-notification-test/scripts/fix-capacitor-plugins.js +++ b/test-apps/daily-notification-test/scripts/fix-capacitor-plugins.js @@ -35,13 +35,17 @@ const PLUGIN_ENTRY = { function fixCapacitorPlugins() { console.log('🔧 Fixing capacitor.plugins.json...'); + // Check if the file exists - if not, it means Capacitor hasn't been synced yet + if (!fs.existsSync(PLUGINS_JSON_PATH)) { + console.log('ℹ️ capacitor.plugins.json not found (Capacitor not synced yet - will be fixed after cap sync)'); + return; + } + try { // Read current content let plugins = []; - if (fs.existsSync(PLUGINS_JSON_PATH)) { - const content = fs.readFileSync(PLUGINS_JSON_PATH, 'utf8'); - plugins = JSON.parse(content); - } + const content = fs.readFileSync(PLUGINS_JSON_PATH, 'utf8'); + plugins = JSON.parse(content); // Check if our plugin is already there const hasPlugin = plugins.some(p => p.name === PLUGIN_ENTRY.name); @@ -56,7 +60,8 @@ function fixCapacitorPlugins() { } catch (error) { console.error('❌ Error fixing capacitor.plugins.json:', error.message); - process.exit(1); + // Don't exit - this might be a first-time install + console.log('ℹ️ This is normal on first install. Run "npm run cap:sync" to generate Capacitor files.'); } } @@ -207,8 +212,9 @@ function fixAll() { fixCapacitorSettingsGradle(); fixPodfile(); - console.log('\n✅ All fixes applied successfully!'); - console.log('💡 These fixes will persist until the next "npx cap sync"'); + console.log('\n✅ Fix script completed!'); + console.log('💡 Note: Some fixes may be skipped if Capacitor files don\'t exist yet.'); + console.log('💡 Run "npm run cap:sync" to generate Capacitor files, then this script will apply fixes.'); } // Run if called directly diff --git a/test-apps/daily-notification-test/scripts/pod-install.sh b/test-apps/daily-notification-test/scripts/pod-install.sh new file mode 100755 index 0000000..fc9c32e --- /dev/null +++ b/test-apps/daily-notification-test/scripts/pod-install.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Wrapper script to find and run pod install +# Handles rbenv shims and standard CocoaPods installations +# +# @author Matthew Raymer + +set -e + +# Get pod command (handles rbenv) +get_pod_command() { + if command -v pod &> /dev/null; then + echo "pod" + elif [ -f "$HOME/.rbenv/shims/pod" ]; then + echo "$HOME/.rbenv/shims/pod" + else + echo "pod" # Let it fail with standard error message + fi +} + +# Find pod command +POD_CMD=$(get_pod_command) + +# Run pod install +exec "$POD_CMD" install "$@" diff --git a/test-apps/daily-notification-test/src/App.vue b/test-apps/daily-notification-test/src/App.vue index 7ac07e4..22a88e6 100644 --- a/test-apps/daily-notification-test/src/App.vue +++ b/test-apps/daily-notification-test/src/App.vue @@ -63,6 +63,7 @@ export default toNative(App) min-height: 100vh; display: flex; flex-direction: column; + padding: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } diff --git a/test-apps/daily-notification-test/src/components/StatusList.vue b/test-apps/daily-notification-test/src/components/StatusList.vue new file mode 100644 index 0000000..413b39e --- /dev/null +++ b/test-apps/daily-notification-test/src/components/StatusList.vue @@ -0,0 +1,102 @@ + + + + + + + + diff --git a/test-apps/daily-notification-test/src/lib/diagnostics-export.ts b/test-apps/daily-notification-test/src/lib/diagnostics-export.ts index 0a612d4..1b2c2e4 100644 --- a/test-apps/daily-notification-test/src/lib/diagnostics-export.ts +++ b/test-apps/daily-notification-test/src/lib/diagnostics-export.ts @@ -8,6 +8,7 @@ * @version 1.0.0 */ +import { Capacitor } from '@capacitor/core' import { type PermissionStatus, type NotificationStatus, @@ -119,10 +120,17 @@ export class DiagnosticsExporter { // Calculate performance metrics const loadTime = performance.now() - startTime + // Detect platform using Capacitor + const platform = Capacitor.getPlatform() + const platformDisplayName = platform.charAt(0).toUpperCase() + platform.slice(1) + + // API Level is Android-specific, show N/A for iOS/web + const apiLevel = platform === 'android' ? 'Unknown' : 'N/A' + return { appVersion: '1.0.0', // TODO: Get from app info - platform: 'Android', // TODO: Detect platform - apiLevel: 'Unknown', // TODO: Get from device info + platform: platformDisplayName, + apiLevel: apiLevel, // TODO: Get from device info for Android timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, lastUpdated: new Date().toLocaleString(), postNotificationsGranted: permissions.notifications === 'granted', diff --git a/test-apps/daily-notification-test/src/router/index.ts b/test-apps/daily-notification-test/src/router/index.ts index 89a7342..a471c1c 100644 --- a/test-apps/daily-notification-test/src/router/index.ts +++ b/test-apps/daily-notification-test/src/router/index.ts @@ -77,6 +77,15 @@ const router = createRouter({ requiresAuth: false } }, + { + path: '/request-permissions', + name: 'RequestPermissions', + component: (): Promise => import('../views/RequestPermissionsView.vue'), + meta: { + title: 'Request Permissions', + requiresAuth: false + } + }, { path: '/about', name: 'About', diff --git a/test-apps/daily-notification-test/src/views/AboutView.vue b/test-apps/daily-notification-test/src/views/AboutView.vue index 756ad2a..841a4bf 100644 --- a/test-apps/daily-notification-test/src/views/AboutView.vue +++ b/test-apps/daily-notification-test/src/views/AboutView.vue @@ -1,10 +1,61 @@ - diff --git a/test-apps/daily-notification-test/src/views/HistoryView.vue b/test-apps/daily-notification-test/src/views/HistoryView.vue index 6bc6fd5..2f985c9 100644 --- a/test-apps/daily-notification-test/src/views/HistoryView.vue +++ b/test-apps/daily-notification-test/src/views/HistoryView.vue @@ -1,7 +1,12 @@