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
This commit is contained in:
Jose Olarte III
2026-03-20 21:13:50 +08:00
parent 1389a166fa
commit e121db5fcf
5 changed files with 330 additions and 22 deletions

View File

@@ -0,0 +1,117 @@
# 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 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):
```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` ~23972411: 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 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.