feat(notifications): enable foreground notifications and rollover recovery

- iOS: set UNUserNotificationCenter delegate and implement willPresent
  so notifications show in foreground and DailyNotificationDelivered is
  posted for rollover; implement didReceive for tap handling; re-set
  delegate in applicationDidBecomeActive
- Android: move DailyNotificationReceiver and BootReceiver inside
  <application>; add NotifyReceiver; extend BootReceiver with
  LOCKED_BOOT_COMPLETED, MY_PACKAGE_REPLACED, directBootAware
- main.capacitor: import daily-notification-plugin at startup so
  plugin (and recovery) load on launch
- doc: add daily-notification-alignment-outline.md

Fixes foreground notifications not showing and rollover recovery;
Android receivers were previously declared outside <application>.
This commit is contained in:
Jose Olarte III
2026-01-29 21:10:34 +08:00
parent fb4ea08f3c
commit 22c3ac80c2
4 changed files with 172 additions and 20 deletions

View File

@@ -43,6 +43,36 @@
</intent-filter>
</activity>
<!-- Daily Notification Plugin Receivers (must be inside application) -->
<receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" />
</intent-filter>
</receiver>
<receiver
android:name="com.timesafari.dailynotification.NotifyReceiver"
android:enabled="true"
android:exported="false">
</receiver>
<receiver
android:name="com.timesafari.dailynotification.BootReceiver"
android:enabled="true"
android:exported="true"
android:directBootAware="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
@@ -52,25 +82,6 @@
</provider>
</application>
<!-- Daily Notification Plugin Receivers -->
<!-- ⚠️ CRITICAL: NotifyReceiver is required for alarm-based notifications -->
<receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true"
android:exported="false">
</receiver>
<!-- Boot receiver to restore notification schedules after device restart -->
<receiver
android:name="com.timesafari.dailynotification.BootReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />

View File

@@ -0,0 +1,106 @@
# Daily Notification Plugin: Alignment Outline
**Purpose:** Checklist of changes/additions needed in this app to align with the test app (`daily-notification-plugin/test-apps/daily-notification-test`) so that:
1. **Rollover recovery** (and rollover itself) works.
2. **Notifications show when the app is in the foreground** (not only background/closed).
3. **Plugin loads at app launch** so recovery runs after reboot without the user opening notification UI.
**Reference:** Test app at
`/Users/aardimus/Sites/trentlarson/daily-notification-plugin_test/daily-notification-plugin/test-apps/daily-notification-test`
---
## 1. iOS AppDelegate
**File:** `ios/App/App/AppDelegate.swift`
### 1.1 Add imports
- [ ] `import UserNotifications`
- [ ] Import the Daily Notification plugin framework (Swift module name: **TimesafariDailyNotificationPlugin** per this apps Podfile; test app uses **DailyNotificationPlugin**)
### 1.2 Conform to `UNUserNotificationCenterDelegate`
- [ ] Add `, UNUserNotificationCenterDelegate` to the class declaration:
`class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate`
### 1.3 Force-load plugin at launch
- [ ] In `application(_:didFinishLaunchingWithOptions:)`, **before** other setup, add logic to force-load the plugin class (e.g. `_ = DailyNotificationPlugin.self` or the class exposed by the TimesafariDailyNotificationPlugin pod) so that the plugins `load()` (and thus `performRecovery()`) runs at app launch, not only when JS first calls the plugin.
### 1.4 Set notification center delegate
- [ ] In `didFinishLaunchingWithOptions`, set:
`UNUserNotificationCenter.current().delegate = self`
- [ ] In `applicationDidBecomeActive`, **re-set** the same delegate (in case Capacitor or another component clears it).
### 1.5 Implement `userNotificationCenter(_:willPresent:withCompletionHandler:)`
- [ ] When a notification is delivered (including in foreground), read `notification_id` and `scheduled_time` from `notification.request.content.userInfo`.
- [ ] Post rollover event:
`NotificationCenter.default.post(name: NSNotification.Name("DailyNotificationDelivered"), object: nil, userInfo: ["notification_id": id, "scheduled_time": scheduledTime])`
- [ ] Call completion handler with presentation options so the notification is shown in foreground, e.g.
`completionHandler([.banner, .sound, .badge])` (use `.alert` on iOS 13 if needed).
### 1.6 Implement `userNotificationCenter(_:didReceive:withCompletionHandler:)`
- [ ] Handle notification tap/interaction; call `completionHandler()` when done.
---
## 2. Android Manifest
**File:** `android/app/src/main/AndroidManifest.xml`
### 2.1 Fix receiver placement
- [ ] Move the two `<receiver>` elements (**DailyNotificationReceiver** and **BootReceiver**) **inside** the `<application>` block (e.g. after `<activity>...</activity>` and before `<provider>...</provider>`).
- [ ] Remove the stray second `</application>` so there is a single `<application>...</application>` containing activity, receivers, and provider.
### 2.2 (Optional) Add NotifyReceiver
- [ ] If the plugins Android integration expects **NotifyReceiver** for alarm-based delivery, add a `<receiver>` for `com.timesafari.dailynotification.NotifyReceiver` inside `<application>` (see test app manifest for exact declaration).
### 2.3 (Optional) BootReceiver options
- [ ] Consider aligning with test app: add `android:directBootAware="true"`, `android:exported="true"`, and intent-filter actions `LOCKED_BOOT_COMPLETED`, `MY_PACKAGE_REPLACED`, `PACKAGE_REPLACED` if you need the same boot/update behavior.
---
## 3. Capacitor / JS startup (optional but recommended)
**File:** `src/main.capacitor.ts` (or the main entry used for native builds)
### 3.1 Load plugin at startup
- [ ] Add a top-level import or an early call that touches the Daily Notification plugin so the JS side loads it at app startup (e.g. `import "@timesafari/daily-notification-plugin"` or a small init that calls `getRebootRecoveryStatus()` or `configure()`).
This ensures the plugin is loaded as soon as the app runs; together with the iOS force-load in AppDelegate, recovery runs at launch.
---
## 4. Plugin configuration (optional)
- [ ] If you use the native fetcher or need plugin config (db path, storage, etc.), call `DailyNotification.configure()` and/or `configureNativeFetcher()` when appropriate (e.g. after login or when notification UI is first used), similar to the test apps `configureNativeFetcher()` in HomeView.
---
## 5. Summary table
| Area | Change / addition |
|-------------------------|------------------------------------------------------------------------------------|
| **iOS AppDelegate** | Conform to `UNUserNotificationCenterDelegate`; set delegate; force-load plugin; implement `willPresent` (post `DailyNotificationDelivered` + show in foreground) and `didReceive`. |
| **Android manifest** | Move DailyNotificationReceiver and BootReceiver inside `<application>`; remove duplicate `</application>`; optionally add NotifyReceiver and BootReceiver options. |
| **main.capacitor.ts** | Optionally import or call plugin at startup so it (and recovery) load at launch. |
| **Plugin config** | Optionally call `configure()` / `configureNativeFetcher()` where appropriate. |
---
## 6. Verification
After making the changes:
- [ ] **iOS:** Build and run; trigger a daily notification and confirm it appears when the app is in the foreground.
- [ ] **iOS:** Confirm rollover (next days schedule) still occurs after a notification fires (check logs for `DNP-ROLLOVER` / `DailyNotificationDelivered`).
- [ ] **iOS:** Restart the app (or reboot) and confirm recovery runs without opening the notification settings screen (e.g. logs show plugin load and recovery).
- [ ] **Android:** Build and run; confirm receivers are registered (no manifest errors) and that notifications and boot recovery behave as expected.

View File

@@ -1,13 +1,17 @@
import UIKit
import Capacitor
import CapacitorCommunitySqlite
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 {
// Set notification center delegate so notifications show in foreground and rollover is triggered
UNUserNotificationCenter.current().delegate = self
// Initialize SQLite
//let sqlite = SQLite()
//sqlite.initialize()
@@ -73,10 +77,38 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
// Re-set notification delegate when app becomes active (in case Capacitor resets it)
UNUserNotificationCenter.current().delegate = self
// Check for shared image from Share Extension when app becomes active
checkForSharedImageOnActivation()
}
// MARK: - UNUserNotificationCenterDelegate
/// Show notifications when app is in foreground and post DailyNotificationDelivered for rollover.
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
if let notificationId = userInfo["notification_id"] as? String,
let scheduledTime = userInfo["scheduled_time"] as? Int64 {
NotificationCenter.default.post(
name: NSNotification.Name("DailyNotificationDelivered"),
object: nil,
userInfo: ["notification_id": notificationId, "scheduled_time": scheduledTime]
)
}
if #available(iOS 14.0, *) {
completionHandler([.banner, .sound, .badge])
} else {
completionHandler([.alert, .sound, .badge])
}
}
/// Handle notification tap/interaction.
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
completionHandler()
}
/**
* Check for shared image when app launches or becomes active
* This allows the app to detect shared images without requiring a deep link

View File

@@ -41,6 +41,9 @@ import { SHARED_PHOTO_BASE64_KEY } from "./libs/util";
import { SharedImage } from "./plugins/SharedImagePlugin";
import "./utils/safeAreaInset";
// Load Daily Notification plugin at startup so native performRecovery() runs at launch (rollover recovery)
import "@timesafari/daily-notification-plugin";
logger.log("[Capacitor] 🚀 Starting initialization");
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);