- Android: move plugin source to org/timesafari/dailynotification, update
namespace, manifest package, and all package/imports; change intent actions
to org.timesafari.daily.NOTIFICATION and DISMISS
- iOS: update bundle IDs, BGTask identifiers, subsystem labels, and queue
names in Plugin and Xcode projects
- Capacitor: update plugin class registration and appIds in configs
- Test apps (android-test-app, daily-notification-test, ios-test-app):
applicationId/bundleId, manifests, ProGuard, scripts, and docs
- Docs: bulk update references; add CONSUMING_APP_MIGRATION_COM_TO_ORG.md
for consuming app migration
BREAKING CHANGE: Consuming apps must update plugin class to
org.timesafari.dailynotification.DailyNotificationPlugin, manifest
receivers/actions, and iOS BGTask identifiers per migration doc.
Remove the guard that opened system Settings and rejected when exact alarms
were not granted. Scheduling now proceeds using inexact/windowed fallback;
consuming apps can handle UX (e.g. optional hint or openExactAlarmSettings()).
- Bug 1: When the firing run used schedule_id daily_rollover_*, resolve the
canonical notify schedule (first enabled with rolloverIntervalMinutes > 0)
and use it to read the interval so the next run is current + interval
instead of +24h. Add ScheduleHelper.getCanonicalRolloverScheduleBlocking().
- Bug 2: For ROLLOVER_ON_FIRE, do not skip scheduling when an existing
PendingIntent is found for the same schedule id: cancel the existing alarm
and set the new trigger time so the rollover chain (e.g. 21:10 → 21:20)
is updated instead of treated as duplicate.
Align package.json and all plugin version references (Android entity
strings and file headers, TypeScript definitions/observability/web)
with 1.3.0 for the rolloverIntervalMinutes release.
Add optional rolloverIntervalMinutes to scheduleDailyNotification so the
next occurrence can be scheduled N minutes after the current trigger
(e.g. 10 minutes) instead of 24 hours. Value is persisted and used on
rollover and after reboot.
- TypeScript: NotificationOptions.rolloverIntervalMinutes?: number
- Android: Schedule.rolloverIntervalMinutes in Room (migration 2→3);
Plugin and ScheduleHelper persist it; Worker uses it in rollover and
updates nextRunAt; ReactivationManager uses it in boot recovery
- iOS: NotificationContent.rolloverIntervalMinutes (Codable); Plugin
passes it into content; Scheduler uses it in calculateNextScheduledTime
and copies to nextContent on rollover
When absent or ≤0, behavior unchanged (24h). App can clear by calling
scheduleDailyNotification without the parameter.
Update package.json, iOS podspec, and Android plugin-version references
after fix for duplicate fallback notifications (cancel fetch-related
WorkManager jobs when scheduling daily notification).
Prevents a second notification (UUID alarm) with fallback or placeholder text by
cancelling pending prefetch/fetch work when the user schedules or reschedules.
cleanupExistingNotificationSchedules only cancels alarms for DB schedule IDs;
alarms from DailyNotificationFetchWorker use a UUID and were never cancelled.
Add ScheduleHelper.cancelFetchRelatedWorkManagerJobs() to cancel only the
prefetch and daily_notification_fetch tags (not display, dismiss, or maintenance).
Call it after cleanup and before scheduleDailyNotification. Future fetched-content
flows can use distinct WorkManager tags and will not be affected by this path.
- Receiver: stop reading Room on main thread; pass schedule_id to Worker
so title/body are resolved on a background thread (fixes
db_fallback_failed / "Cannot access database on the main thread").
- Worker: use stable schedule_id for rollover so one alarm per reminder
and reschedule cancels it; resolve user title/body by schedule_id when
Intent lacks them; skip prefetch for static reminders to avoid a
second alarm.
- ScheduleHelper: persist NotificationContentEntity for scheduleId when
scheduling daily notification so rollover and post-reboot show user
text.
Refs: plugin-feedback-android-rollover-double-fire-and-user-content
Boot recovery was skipping reschedule when it found an "existing" PendingIntent.
AlarmManager alarms are not guaranteed to persist across reboot; on devices that
clear them, the skip caused the next notification (initial or rollover) to never
fire until the app was opened. Pass skipPendingIntentIdempotence = true for all
BOOT_RECOVERY call sites (BootReceiver, ReactivationManager.rescheduleAlarmForBoot)
so the alarm is always re-registered after reboot. Setting the same PendingIntent
again replaces any existing alarm, so no duplicate alarms.
After device restart, PendingIntent extras (title, body, is_static_reminder) can be
missing when the alarm fires, so the worker took the Room/JIT path and showed
fallback text instead of the user's message.
- DailyNotificationReceiver: when intent has notification_id but missing title/body,
load NotificationContentEntity from Room and pass title/body into Worker input
with is_static_reminder=true.
- ReactivationManager: add getTitleBodyForSchedule(); use persisted title/body in
rescheduleAlarm and rescheduleAlarmForBoot (and inner boot helper) instead of
hardcoded "Daily Notification" / "Your daily update is ready".
- BootReceiver: use ReactivationManager.getTitleBodyForSchedule() when building
UserNotificationConfig for notify schedules after boot.
- DailyNotificationWorker: when content from Room has both title and body, skip
performJITFreshnessCheck so user text is not overwritten by fetcher placeholder.
Ref: plugin-feedback-android-post-reboot-fallback-text (crowd-funder-for-time-pwa)
Do not enqueue DailyNotificationFetchWorker for static reminder schedules.
Display is already handled by the single NotifyReceiver alarm; prefetch was
using fallback content and scheduling a second alarm via legacy
DailyNotificationScheduler, causing two notifications at fire time.
Remove existingPendingIntent.cancel() in the "cancel existing alarm before
rescheduling" block. The cached PendingIntent can be the same instance passed
to setAlarmClock; cancelling it can prevent the new alarm from firing. Keep
only alarmManager.cancel(existingPendingIntent) to clear the previous alarm.
NotifyReceiver's post-schedule DB update no longer uses the "first enabled
notify schedule" fallback when stableScheduleId starts with "daily_rollover_".
That fallback was updating the app's schedule row (e.g. daily_timesafari_reminder)
with the rollover time and could leave the app's next alarm in a bad state after
a notification fired.
Add docs/CONSUMING_APP_ANDROID_NOTES.md with notes for consuming apps: debounce
double scheduleDailyNotification calls, and include DailyNotificationReceiver
in logcat when debugging alarms that are scheduled but do not fire.
Fixes two integration bugs with the consuming app (Time Safari) and adds
Android parity for cancel-by-id.
Problem:
- Re-setting a daily notification (edit/save same time) could cancel the
alarm then skip re-scheduling because DB idempotence still ran and
treated the update as a duplicate.
- After the first fire, rollover scheduled the next run with
isStaticReminder=false, so title/body reverted to fallback.
- App calls cancelDailyReminder({ reminderId }) but Android had no
implementation (only cancelAllNotifications and scheduleDailyReminder).
Changes:
- NotifyReceiver.kt: Run DB idempotence only when
!skipPendingIntentIdempotence. When true (e.g. app reset flow), skip
the check and log; prevents "no alarm" after cancel-then-schedule.
- DailyNotificationWorker.java: In scheduleNextNotification(), read
is_static_reminder from WorkManager input; keep stable scheduleId for
static reminders; pass preserveStaticReminder and reminderId into
scheduleExactNotification(); add DN|ROLLOVER log.
- DailyNotificationPlugin.kt: Add cancelDailyReminder(call) that parses
reminderId (or id, reminder_id, scheduleId), calls
NotifyReceiver.cancelNotification(context, scheduleId), and does
best-effort DB cleanup (setEnabled false, updateRunTimes null).
Files modified:
- android/.../NotifyReceiver.kt
- android/.../DailyNotificationWorker.java
- android/.../DailyNotificationPlugin.kt
Cancel-then-schedule was skipped because the idempotence check still
found the cancelled PendingIntent in Android's cache. Skip
PendingIntent idempotence on the cancel-then-schedule path so the
new schedule is always set.
- NotifyReceiver.scheduleExactNotification: add
skipPendingIntentIdempotence (used only from scheduleDailyNotification)
- ScheduleHelper: pass skipPendingIntentIdempotence=true after
cancelNotification(scheduleId)
- Version 1.1.2: package.json, CHANGELOG, README, TS/Android refs
- docs/CONSUMING_APP_OPTIONAL_ANDROID_ID_CLEANUP.md: optional app
cleanup to use one stable id on both platforms
Covers USB debugging setup, battery optimization settings for major OEMs
(Samsung, Xiaomi, OnePlus, Huawei, Oppo), log monitoring, and
troubleshooting. Complements EMULATOR_GUIDE for real-device validation.
Set Intent.setPackage(context.packageName) when creating PendingIntents
for AlarmManager so the broadcast is delivered to DailyNotificationReceiver
on all OEMs. Alarms were firing but the receiver was not invoked when the
component was not explicitly package-targeted.
- NotifyReceiver: setPackage on schedule, cancel, and isAlarmScheduled intents
- ReactivationManager: alarmsExist() use DailyNotificationReceiver + setPackage
- DailyNotificationScheduler: setPackage on ExactAlarmManager path intent
EMULATOR_GUIDE.md:
- Add "Checking and Installing Prerequisites" (how to check Node, npm, Java,
ANDROID_HOME, adb, emulator, AVDs; install steps; reference to
scripts/check-environment.js)
- Use API 35 and Pixel8_API35 throughout to match project compileSdk/targetSdk
- Document arm64-v8a for Apple Silicon and x86_64 for Intel; add
troubleshooting for "x86_64 not supported on aarch64 host"
- Bump version to 1.1.0 and last-updated date
test-apps/daily-notification-test/scripts/build.sh:
- When building only Android (--android / --run-android), run
cap:sync:android instead of cap:sync so iOS pod install is skipped and
Android build/run succeeds without fixing the iOS Podfile
- Add "Checking and Installing Prerequisites" section:
- How to check Node, npm, Java, ANDROID_HOME, adb, emulator, AVDs
- Reference scripts/check-environment.js for partial check
- Install steps for Node, Java, Android SDK (cmdline-tools only),
sdkmanager packages, and avdmanager AVD creation
- Align SDK and AVD with project: use API 35 (android-35, build-tools 35.0.0,
Pixel8_API35) to match compileSdk/targetSdk in variables.gradle
- Bump guide version to 1.1.0 and last-updated date
Version bump reflects new features merged from ios-2 branch:
- iOS rollover recovery for background/inactive app scenarios
- Build script improvements and iOS support
- Test app enhancements and UI improvements
This is a MINOR version bump per semantic versioning due to
backward-compatible feature additions.
Updates BUILDING.md to reflect recent changes in build-native.sh, especially
the Xcode Command Line Tools prerequisite check and the clean-build script.
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
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
- 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
- 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
- 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
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:
- BUILDING.md
The cap:sync:ios npm script was failing with "pod: command not found"
because npm scripts don't inherit the same PATH environment as shell
scripts, preventing detection of pod installed via rbenv shims.
Added pod-install.sh wrapper script that:
- Detects pod via command -v or rbenv shims ($HOME/.rbenv/shims/pod)
- Executes pod install with the found command
- Matches the pod detection logic used in build-native.sh
Updated cap:sync:ios script to use the wrapper instead of calling
pod install directly, ensuring consistent pod detection across
all build contexts.
Prevents iOS build failures caused by pkgx SQLite linking conflicts and
ensures Xcode Command Line Tools are properly installed.
Problem:
- pkgx installs SQLite built for macOS, causing linker errors when building
for iOS simulator: "linking in dylib built for 'macOS'"
- Missing Command Line Tools cause build failures without clear error messages
Changes:
- Add check_sqlite_conflicts() function
- Detects pkgx SQLite installations in ~/.pkgx
- Warns about macOS dylibs that will cause iOS simulator build failures
- Checks for system SQLite from Command Line Tools
- Validates library paths (DYLD_LIBRARY_PATH, LD_LIBRARY_PATH)
- Add check_command_line_tools() function
- Verifies Xcode Command Line Tools are installed and configured
- Checks for xcodebuild availability
- Verifies sqlite3 is available (part of Command Line Tools)
- Provides clear error messages with installation instructions
- Enhance pkgx detection in iOS build functions
- Specifically searches for pkgx SQLite dylibs
- Automatically removes pkgx paths from PATH environment variable
- Provides detailed warnings about detected conflicts
- Cleans all problematic environment variables before building
- Integrate checks into environment validation
- Runs automatically when building for iOS
- Provides early warnings before build starts
- Fails fast with clear error messages if tools are missing
This fixes the linker error:
"ld: building for 'iOS-simulator', but linking in dylib
(/Users/trent/.pkgx/sqlite.org/v3.44.2/lib/libsqlite3.0.dylib)
built for 'macOS'"
The build script now:
- Detects pkgx SQLite conflicts before building
- Automatically fixes environment variables
- Verifies Command Line Tools are installed
- Provides clear guidance for manual fixes if needed
Files modified:
- scripts/build-native.sh
Fixed Android test app build failures by ensuring Capacitor is synced
before building and automatically fixing missing file references.
Changes:
- Add Capacitor Android sync step before building test app
- Runs `npm run cap:sync:android` to create required project structure
- Ensures `:capacitor-cordova-android-plugins` project exists before Gradle resolves dependencies
- Add automatic fix for missing cordova.variables.gradle reference
- Detects when capacitor.build.gradle references non-existent file
- Automatically comments out problematic `apply from` line
- Uses platform-agnostic sed command (handles macOS and Linux)
- Provides clear logging about what was fixed
- Reorganize build flow to match iOS pattern
- Sync Capacitor first, then navigate to platform directory
- Apply fixes, then build
This fixes the build error:
"Could not resolve project :capacitor-cordova-android-plugins"
"No matching variant of project :capacitor-cordova-android-plugins was found"
The build now completes successfully even when:
- Capacitor hasn't been synced yet
- capacitor.build.gradle references missing files
Files modified:
- scripts/build-native.sh
Fixed build failures when test app dependencies aren't installed and when
Capacitor files don't exist during initial install.
Changes:
- Use `npx run-p` in test app build script to work without local install
- Add dependency check in build-native.sh before building Vue app
- Make fix-capacitor-plugins.js resilient to missing files during postinstall
- Gracefully handles missing capacitor.plugins.json on first install
- Provides clear messaging about when fixes will be applied
- No longer exits with error when Capacitor hasn't been synced yet
This allows the build to complete successfully even when:
- npm dependencies aren't installed in test app
- Capacitor files don't exist yet (will be created during cap:sync:ios)
Files modified:
- scripts/build-native.sh
- test-apps/daily-notification-test/package.json
- test-apps/daily-notification-test/scripts/fix-capacitor-plugins.js
Fixes iOS build failures caused by linker picking up macOS SQLite
libraries from pkgx instead of iOS system SQLite, resulting in
undefined symbol errors for all sqlite3 functions.
Changes:
- Explicitly link system SQLite library (-lsqlite3) in podspec
- Detect and unset pkgx environment variables during iOS builds
- Add warnings to guide users if manual intervention needed
The issue occurs when pkgx (or similar package managers) set
DYLD_LIBRARY_PATH or PKGX_DIR, causing the linker to find macOS
SQLite dylibs at /Users/*/.pkgx/sqlite.org/*/lib/libsqlite3.0.dylib
instead of the iOS system SQLite library.
This fix ensures the iOS build always uses the correct system SQLite
library regardless of environment variable interference.
Adds a comprehensive clean script that removes all build artifacts,
caches, and dependencies to help reproduce build issues across
different development environments.
The script cleans:
- TypeScript build output (dist/)
- iOS plugin artifacts (Pods, Podfile.lock, build dirs)
- Android plugin artifacts (build dirs, optional .gradle cache)
- Test app artifacts (node_modules, dist/, iOS/Android builds)
- Optional: Xcode DerivedData, Gradle cache, node_modules reinstall
Usage:
./scripts/clean-build.sh # Basic clean
./scripts/clean-build.sh --all # Full clean with reinstall
This is particularly useful when troubleshooting build failures
that may be environment-specific (different Xcode, CocoaPods,
macOS, or Node versions).
Implement enhanced app launch recovery to detect and schedule missed rollover
notifications that occurred while the app was terminated, backgrounded, or
inactive.
Key improvements:
- Detect missed rollovers on app launch by checking for past notifications
without next scheduled notification
- Add active rollover check when app becomes active (handles inactive app
scenario where notifications fire silently)
- Calculate forward to future time when next scheduled time is in the past
(handles delays > rollover interval)
- Enhance duplicate detection to exclude original notification from checks
- Retry rollover if previous attempt failed (rollover time set but no next
notification exists)
Changes:
- DailyNotificationReactivationManager: Add detectAndProcessMissedRollovers()
method and performActiveRolloverCheck() for app becoming active
- DailyNotificationReactivationManager: Enhance warm start scenario to check
for missed rollovers
- DailyNotificationScheduler: Add forward calculation loop when next scheduled
time is in the past
- DailyNotificationPlugin: Register observer for UIApplication.didBecomeActiveNotification
to trigger rollover check when app becomes active
Fixes rollover scheduling for:
- App terminated: Rollover now detected and scheduled on next launch
- App inactive/backgrounded: Rollover detected when app becomes active
- Delayed recovery: Handles cases where app reopened after rollover interval
has passed by calculating forward to next future time
All scenarios now properly schedule rollover notifications regardless of app
state when notification fires.
Add inline comments and documentation explaining how to temporarily
change rollover notification intervals from 24 hours to 2 minutes
for testing purposes. Comments specify exact line numbers and values
to change, making it easy to switch between production and testing
modes without losing context.
Changes:
- Add TESTING section to calculateNextScheduledTime() documentation
- Add inline TESTING comments at three change points:
* Calendar date addition (24 hours → 2 minutes)
* Fallback time calculation (24 hours → 2 minutes)
* Duplicate prevention threshold (1 hour → 1 minute)
All code remains at production settings (24-hour intervals).