- Treat updateStarredPlans as optional: catch UNIMPLEMENTED and continue to scheduleDualNotification so missing native method no longer blocks scheduling. - Show specific toast when BGTaskSchedulerErrorDomain error 1 occurs (e.g. Simulator): explain that a real device and Background App Refresh are required. - Add PluginHeaders diagnostic in AccountViewView and main.capacitor.ts to debug UNIMPLEMENTED (log DNP methods at call time and at launch). - Fix main.capacitor.ts build: use CapacitorWindow type and safe cap assignment so vite build --mode capacitor succeeds. - Docs: add UNIMPLEMENTED troubleshooting and updateStarredPlans note in plugin-feedback-ios-scheduleDualNotification.md; add section 8.3 in notification-new-activity-lay-of-the-land.md. - Lockfile updates (package-lock.json, Podfile.lock).
9.5 KiB
Plugin Feedback: Implement scheduleDualNotification on iOS
Target repo: daily-notification-plugin (iOS native layer)
Purpose: Document for implementing or fixing scheduleDualNotification on iOS so the consuming app (TimeSafari / crowd-funder) can enable “New Activity” notifications.
Consuming app doc: doc/notification-new-activity-lay-of-the-land.md
Troubleshooting: UNIMPLEMENTED on iOS (Capacitor 6)
If configureNativeFetcher (or other DailyNotification methods) work but scheduleDualNotification still fails with {"code":"UNIMPLEMENTED"} and you do not see a native log line like To Native -> DailyNotification scheduleDualNotification, the failure is often not missing Swift code—it is Capacitor’s JavaScript layer rejecting the call because the method is not listed in window.Capacitor.PluginHeaders for DailyNotification. Those headers are built at runtime from the compiled plugin’s pluginMethods list (CAPBridgedPlugin).
Fix in the consuming app (usual cause: stale Pods / binary):
- Ensure
node_modules/@timesafari/daily-notification-pluginincludesscheduleDualNotificationinDailyNotificationPlugin.swift’spluginMethods(v2.1.0+). - From the project root:
npx cap sync ios cd ios/App && pod install(or deletePods+Podfile.lockandpod installif upgrading the plugin).- Xcode: Product → Clean Build Folder, then rebuild and run on device/simulator.
Verify: Safari → Develop → attach to the app WebView → Console: inspect window.Capacitor.PluginHeaders and confirm the DailyNotification entry’s methods array includes { name: "scheduleDualNotification", ... }.
If a full clean rebuild still doesn't fix it, clear Xcode's system DerivedData (quit Xcode, run rm -rf ~/Library/Developer/Xcode/DerivedData/*TimeSafari*, reopen and rebuild). On launch the app logs [Capacitor] DNP PluginHeaders methods: [...]; if that list omits scheduleDualNotification, the native binary is still stale.
If the method is present in headers but scheduling still fails, debug the Swift implementation (reject message, BG tasks, etc.).
Misleading UNIMPLEMENTED before scheduleDualNotification
Capacitor’s registerPlugin proxy returns a callable stub for every property name. So if (DailyNotification?.updateStarredPlans) is always truthy even when iOS does not expose updateStarredPlans in pluginMethods. Calling that stub throws UNIMPLEMENTED in JS before any To Native -> DailyNotification scheduleDualNotification line appears—so logs look like “dual schedule is unimplemented” when the real failure was updateStarredPlans.
Consuming-app fix: treat updateStarredPlans as optional: catch UNIMPLEMENTED and continue, or only call after verifying the method name exists on PluginHeaders for DailyNotification. If the plugin adds updateStarredPlans natively later, starred-plan filtering will start working without app changes.
Current behavior
- The consuming app calls
DailyNotification.scheduleDualNotification({ config })from TypeScript when the user turns on “New Activity Notification” and picks a time (native iOS). - On iOS, the plugin rejects with
code: "UNIMPLEMENTED"(observed in Xcode:[AccountViewView] scheduleNewActivityDualNotification failed: {"code":"UNIMPLEMENTED"}). - On Android, the same call is expected to work (dual schedule: content fetch + user notification).
The app has already:
- Called
configureNativeFetcher({ apiBaseUrl, activeDid, jwtToken })so the plugin can use the native fetcher for API-driven content. - Called
updateStarredPlans({ planIds })so the fetcher knows which plans to query. - Built a
configobject that matches the plugin’sDualScheduleConfiguration(see below).
So the missing piece on iOS is a working implementation of scheduleDualNotification that accepts this config and schedules the dual flow (content fetch at one time, user notification at a later time).
Call from the consuming app
await DailyNotification.scheduleDualNotification({ config });
config is built by the app’s buildDualScheduleConfig({ notifyTime }) and has the following shape.
Config shape the app sends
The app sends a single config object that matches the plugin’s DualScheduleConfiguration (see definitions.ts). Example for notifyTime: "18:30" (6:30 PM):
{
"contentFetch": {
"enabled": true,
"schedule": "25 18 * * *",
"callbacks": {}
},
"userNotification": {
"enabled": true,
"schedule": "30 18 * * *",
"title": "New Activity",
"body": "Check your starred projects and offers for updates.",
"sound": true,
"priority": "normal"
},
"relationship": {
"autoLink": true,
"contentTimeout": 300000,
"fallbackBehavior": "show_default"
}
}
- Cron format:
"minute hour * * *"(daily at that local time). - contentFetch.schedule: 5 minutes before the user’s chosen time (e.g. 18:25 for notify at 18:30).
- userNotification.schedule: The user’s chosen time (e.g. 18:30).
- contentFetch.callbacks: The app sends
{}; the actual fetch is done by the native fetcher (already configured viaconfigureNativeFetcher). The plugin should run the content-fetch job at the contentFetch cron and use the native fetcher to get content; at userNotification time it should show a notification using that content or the fallback title/body. - relationship.contentTimeout: Milliseconds to wait for content before showing the notification (app uses 5 minutes = 300000).
- relationship.fallbackBehavior:
"show_default"means if content isn’t ready in time, show the notification with the default title/body fromuserNotification.
The app does not send contentFetch.url or contentFetch.timesafariConfig; it relies on the native fetcher and configureNativeFetcher / updateStarredPlans for API behavior.
Expected plugin behavior (iOS)
- Accept the
configargument (object withcontentFetch,userNotification, and optionalrelationship). - Parse the cron expressions for
contentFetch.scheduleanduserNotification.schedule(e.g. using a shared cron parser or the same approach as Android). - Schedule two things:
- Content fetch: At the time given by
contentFetch.schedule, run the native notification content fetcher (the one configured viaconfigureNativeFetcher). Store the result in the plugin’s cache (or equivalent) for use when the user notification fires. - User notification: At the time given by
userNotification.schedule, show a local notification. Use cached content from the fetch if available and withinrelationship.contentTimeout; otherwise useuserNotification.titleanduserNotification.body(perrelationship.fallbackBehavior: "show_default").
- Content fetch: At the time given by
- Do not reject with
UNIMPLEMENTED; resolve the promise once scheduling has succeeded (or reject with a descriptive error if scheduling fails). - cancelDualSchedule() should cancel both the content-fetch schedule and the user-notification schedule so the user can turn off New Activity from the app.
Alignment with Android (if implemented there) is desirable: same config shape, same semantics (prefetch then notify, fallback to default title/body). The plugin’s definitions.ts already defines DualScheduleConfiguration, ContentFetchConfig, UserNotificationConfig, and the scheduleDualNotification / cancelDualSchedule API.
Where to look in the plugin (iOS)
- Plugin entry:
ios/Plugin/DailyNotificationPlugin.swift(or equivalent)—find the handler forscheduleDualNotification(e.g. method that receivescall.getObject("config")). - Android reference:
android/implementation ofscheduleDualNotificationand how it schedules WorkManager/alarms for content fetch and for the user notification. - Definitions:
src/definitions.ts—DualScheduleConfiguration,scheduleDualNotification,cancelDualSchedule. - Native fetcher: The app configures the native fetcher before calling
scheduleDualNotification; the iOS plugin should invoke that same fetcher when the content-fetch job runs (BGAppRefreshTask or equivalent), not a URL from the config.
Acceptance criteria
- On iOS, calling
DailyNotification.scheduleDualNotification({ config })with the config shape above does not reject withcode: "UNIMPLEMENTED". - The content-fetch job is scheduled at
contentFetch.scheduleand uses the configured native fetcher to fetch content. - The user notification is scheduled at
userNotification.scheduleand shows with API-derived content when available, or withuserNotification.title/userNotification.bodyas fallback. - Calling
DailyNotification.cancelDualSchedule()cancels both schedules on iOS. - Behavior is consistent with Android where applicable (same config, same lifecycle).
Relationship to consuming app
The consuming app will continue to call:
configureNativeFetcher(...)on startup and when enabling New Activity.updateStarredPlans({ planIds })when enabling or when Account view loads with New Activity on.scheduleDualNotification({ config })when the user turns on New Activity and picks a time.cancelDualSchedule()when the user turns off New Activity.
No change to the app’s config shape or call order is planned; the fix is entirely on the plugin iOS side to implement or correct scheduleDualNotification (and ensure cancelDualSchedule clears the dual schedule).