Files
crowd-funder-for-time-pwa/doc/plugin-feedback-ios-scheduleDualNotification.md
Jose Olarte III 1389a166fa fix(ios): New Activity dual notification – handle updateStarredPlans and BGTaskScheduler errors
- 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).
2026-03-19 19:26:59 +08:00

9.5 KiB
Raw Blame History

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 Capacitors 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 plugins pluginMethods list (CAPBridgedPlugin).

Fix in the consuming app (usual cause: stale Pods / binary):

  1. Ensure node_modules/@timesafari/daily-notification-plugin includes scheduleDualNotification in DailyNotificationPlugin.swifts pluginMethods (v2.1.0+).
  2. From the project root: npx cap sync ios
  3. cd ios/App && pod install (or delete Pods + Podfile.lock and pod install if upgrading the plugin).
  4. 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 entrys 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

Capacitors 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 config object that matches the plugins DualScheduleConfiguration (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 apps buildDualScheduleConfig({ notifyTime }) and has the following shape.


Config shape the app sends

The app sends a single config object that matches the plugins 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 users chosen time (e.g. 18:25 for notify at 18:30).
  • userNotification.schedule: The users chosen time (e.g. 18:30).
  • contentFetch.callbacks: The app sends {}; the actual fetch is done by the native fetcher (already configured via configureNativeFetcher). 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 isnt ready in time, show the notification with the default title/body from userNotification.

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)

  1. Accept the config argument (object with contentFetch, userNotification, and optional relationship).
  2. Parse the cron expressions for contentFetch.schedule and userNotification.schedule (e.g. using a shared cron parser or the same approach as Android).
  3. Schedule two things:
    • Content fetch: At the time given by contentFetch.schedule, run the native notification content fetcher (the one configured via configureNativeFetcher). Store the result in the plugins 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 within relationship.contentTimeout; otherwise use userNotification.title and userNotification.body (per relationship.fallbackBehavior: "show_default").
  4. Do not reject with UNIMPLEMENTED; resolve the promise once scheduling has succeeded (or reject with a descriptive error if scheduling fails).
  5. 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 plugins 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 for scheduleDualNotification (e.g. method that receives call.getObject("config")).
  • Android reference: android/ implementation of scheduleDualNotification and how it schedules WorkManager/alarms for content fetch and for the user notification.
  • Definitions: src/definitions.tsDualScheduleConfiguration, 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 with code: "UNIMPLEMENTED".
  • The content-fetch job is scheduled at contentFetch.schedule and uses the configured native fetcher to fetch content.
  • The user notification is scheduled at userNotification.schedule and shows with API-derived content when available, or with userNotification.title / userNotification.body as 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:

  1. configureNativeFetcher(...) on startup and when enabling New Activity.
  2. updateStarredPlans({ planIds }) when enabling or when Account view loads with New Activity on.
  3. scheduleDualNotification({ config }) when the user turns on New Activity and picks a time.
  4. cancelDualSchedule() when the user turns off New Activity.

No change to the apps 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).