Files
crowd-funder-for-time-pwa/doc/plugin-feedback-android-scheduleDualNotification-contentFetch-json.md
Jose Olarte III e121db5fcf fix(notifications): align dual schedule config with Android plugin + bump DNP
- buildDualScheduleConfig: set contentFetch timeout/retryAttempts/retryDelay
  (match capacitor DailyNotification networkConfig), userNotification.vibration,
  return type DualScheduleConfiguration
- @timesafari/daily-notification-plugin 2.1.1 → 2.1.3 (package-lock)
- doc: plugin feedback (contentFetch JSON, parseUserNotificationConfig optional
  fields) and Android DailyNotificationWorker duplicate scheduleId note
2026-03-20 21:13:50 +08:00

6.1 KiB
Raw Blame History

Plugin feedback: Android scheduleDualNotificationJSONException: No value for timeout

Date: 2026-03-20 18:21 PST
Target repo: @timesafari/daily-notification-plugin (daily-notification-plugin)
Consuming app: crowd-funder-for-time-pwa (TimeSafari)
Platform: Android (Kotlin)
Plugin version observed: 2.1.2 (from app node_modules)


Summary

Scheduling the New Activity dual notification on Android fails with a native JSONException because DailyNotificationPlugin.parseContentFetchConfig() uses JSONObject.getInt() for timeout, retryAttempts, and retryDelay. Those keys are absent from the apps contentFetch object built by buildDualScheduleConfig(). The plugins own TypeScript ContentFetchConfig marks those fields as optional, so the Android parser is stricter than the published contract.

Recommended direction:

  1. Plugin (primary): Parse optional numeric fields with defaults (e.g. optInt / nullable + defaults) so payloads that omit them do not crash and match definitions.d.ts.
  2. App (secondary / compatibility): Include explicit timeout, retryAttempts, and retryDelay on contentFetch so older plugin versions that still use getInt continue to work.

Does it make sense to change both sides? Yes. Fixing the plugin aligns behavior with the documented API and protects any consumer that omits those fields. Fixing the app is still valuable for older shipped plugin builds and makes network behavior explicit. Together you get backward compatibility, clearer intent, and no silent reliance on undocumented defaults.


Symptoms (consuming app)

  • In-app toast: “Could not schedule New Activity notification. Please try again.” (generic error path after scheduleDualNotification rejects.)
  • Logcat (filtered on DNP / plugin tags):
E DNP-PLUGIN: Schedule dual notification error
E DNP-PLUGIN: org.json.JSONException: No value for timeout
E DNP-PLUGIN:   at org.json.JSONObject.getInt(JSONObject.java:487)
E DNP-PLUGIN:   at org.timesafari.dailynotification.DailyNotificationPlugin.parseContentFetchConfig(DailyNotificationPlugin.kt:2403)
E DNP-PLUGIN:   at org.timesafari.dailynotification.DailyNotificationPlugin.scheduleDualNotification(DailyNotificationPlugin.kt:1391)

Root cause

Call path

scheduleDualNotification reads config.contentFetch and passes it to parseContentFetchConfig:

  • File: android/.../DailyNotificationPlugin.kt
  • scheduleDualNotification ~1391: parseContentFetchConfig(contentFetchObj)
  • parseContentFetchConfig ~23972411: uses getInt for three keys.

Strict Android parsing

Illustrative (exact line numbers may shift between releases):

// parseContentFetchConfig — timeout / retry fields are required via getInt()
timeout = configJson.getInt("timeout"),
retryAttempts = configJson.getInt("retryAttempts"),
retryDelay = configJson.getInt("retryDelay"),

getInt throws if the key is missing → first missing key in practice is timeoutJSONException: No value for timeout.

App payload today (consuming app)

File: src/services/notifications/dualScheduleConfig.tsbuildDualScheduleConfig() sets contentFetch to:

  • enabled, schedule, callbacks only (no timeout, retryAttempts, retryDelay, no url).

That matches the TypeScript contract in the plugins dist/esm/definitions.d.ts, where timeout, retryAttempts, and retryDelay are optional on ContentFetchConfig.

Contract mismatch

Layer timeout / retryAttempts / retryDelay
TS ContentFetchConfig Optional (?)
Android parseContentFetchConfig Required (getInt — throws if absent)

The consuming app followed the TS API; Android rejected it at runtime.


Plugin-side recommendations

  1. Use optional reads with defaults for timeout, retryAttempts, and retryDelay (and any similar fields), e.g. Kotlin/Capacitor equivalents of optInt or getInteger with fallbacks documented in ContentFetchConfig.
  2. Document defaults in the plugin README or API docs if they are applied on native when omitted.
  3. Consider tests that call scheduleDualNotification with a minimal contentFetch (only enabled, schedule, callbacks) and assert scheduling succeeds on Android.
  4. Optional: If url is also read in a way that assumes presence, align with TS (url?) the same way.

App-side recommendations (later; crowd-funder-for-time-pwa)

When you implement the app fix:

  • Extend contentFetch in buildDualScheduleConfig() (src/services/notifications/dualScheduleConfig.ts) to include explicit integers, for example aligned with existing app/network conventions (the apps capacitor.config.ts already uses a timeout value in one place — reuse or document chosen values).
  • Ensure both code paths that build dual config stay in sync (e.g. AccountViewView.vue uses buildDualScheduleConfig for New Activity scheduling and for updateDualScheduleConfig fallback).

This unblocks users on current plugin versions that still require those keys.


References (paths in consuming app workspace)

  • App config builder: src/services/notifications/dualScheduleConfig.ts
  • Native scheduling entry: node_modules/@timesafari/daily-notification-plugin/android/.../DailyNotificationPlugin.kt (scheduleDualNotification, parseContentFetchConfig)

Answer: change both plugin and app?

Yes, it makes sense to change both, for different reasons:

Side Why
Plugin Fixes the real bug: native behavior must match the published optional TS fields; avoids breaking any client that sends a minimal contentFetch.
App Defense in depth and support for already-shipped plugin binaries that will not get the Kotlin fix until users update the app. Explicit values also document intended fetch/retry behavior in one place.

If you only fix the plugin, new app releases still need users to update the native binary. If you only fix the app, any other consumer of the plugin or future minimal payloads can hit the same crash until the plugin is fixed.