# Plugin feedback: Android `scheduleDualNotification` — `JSONException: 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 app’s `contentFetch` object built by `buildDualScheduleConfig()`. The plugin’s 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): ```text 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` ~2397–2411: uses `getInt` for three keys. ### Strict Android parsing Illustrative (exact line numbers may shift between releases): ```kotlin // 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 `timeout` → `JSONException: No value for timeout`. ### App payload today (consuming app) File: `src/services/notifications/dualScheduleConfig.ts` — `buildDualScheduleConfig()` sets `contentFetch` to: - `enabled`, `schedule`, `callbacks` only (no `timeout`, `retryAttempts`, `retryDelay`, no `url`). That matches the **TypeScript** contract in the plugin’s `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 app’s `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.