Add TimeSafariNativeFetcher (plansLastUpdatedBetween parity with Android) and call DailyNotificationPlugin.registerNativeFetcher from AppDelegate before JS configureNativeFetcher; broaden DailyNotificationDelivered scheduled_time types in willPresent. Wire the new file into the App target; normalize PBX object IDs to 24-char hex. Document plugin ≥3 handoff (consuming-app-handoff-ios-native-fetcher-chained-dual), refresh iOS/Android parity and notification-from-api-call file tables.
251 lines
26 KiB
Markdown
251 lines
26 KiB
Markdown
# Lay of the Land: API-Driven Daily Message (New Activity) and Web-Push Confusion
|
||
|
||
**Purpose:** Shareable analysis of the New Activity (API-driven daily message) implementation and the root cause of “always fires / can’t be turned off.” For discussion with teammates.
|
||
|
||
**Related:** `doc/notification-from-api-call.md` (plan and progress), teammate note about web-push confusion and possibly removing that logic.
|
||
|
||
---
|
||
|
||
## 1. Two Separate Notification Features
|
||
|
||
There are **two** distinct native notification flows that both go through the same UI component:
|
||
|
||
| Feature | Plugin API | Purpose |
|
||
|--------|------------|--------|
|
||
| **Daily Reminder** | `scheduleDailyNotification` / `cancelDailyReminder` | Single daily alarm, static title/body (user’s message). |
|
||
| **New Activity** (API-driven) | `scheduleDualNotification` / `cancelDualSchedule` | Prefetch from API 5 min before, then notify at chosen time with API or fallback content. |
|
||
|
||
- **Daily Reminder** is driven from AccountViewView’s “Daily Reminder” toggle; on native it uses `NotificationService.getInstance().scheduleDailyNotification()` / `cancelDailyNotification()` (backed by `NativeNotificationService` and a single `reminderId`: `"daily_timesafari_reminder"`).
|
||
- **New Activity** is intended to be driven only by `scheduleNewActivityDualNotification()` / `cancelDualSchedule()` in AccountViewView (dual schedule only).
|
||
|
||
So: one feature = single schedule (reminder), the other = dual schedule (prefetch + notify). They are different plugin APIs and different lifecycle (enable/disable) handling.
|
||
|
||
---
|
||
|
||
## 2. Where the Bug Comes From: One Dialog, Two Behaviors
|
||
|
||
**New Activity** reuses the same dialog as Daily Reminder: **`PushNotificationPermission.vue`**.
|
||
|
||
- When the user turns **New Activity** on from AccountViewView:
|
||
- AccountViewView opens this dialog with `DAILY_CHECK_TITLE` and a callback that, on success, calls `scheduleNewActivityDualNotification(timeText)` on native.
|
||
- The dialog does **not** receive `skipSchedule: true` for this flow (only the “edit reminder” flow does).
|
||
|
||
So when the user clicks “Turn on Daily Reminder” in the dialog for **New Activity**:
|
||
|
||
1. **PushNotificationPermission** (native path) runs `turnOnNativeNotifications()` and always calls:
|
||
- `service.scheduleDailyNotification({ time, title: "Daily Check-In", body: "Time to check your TimeSafari activity", ... })`
|
||
- i.e. it schedules the **single** daily reminder (plugin’s `scheduleDailyNotification`), using the same `reminderId` as Daily Reminder (`"daily_timesafari_reminder"`).
|
||
2. Then the callback runs and AccountViewView calls **`scheduleNewActivityDualNotification(timeText)`**, which calls the plugin’s **`scheduleDualNotification`**.
|
||
|
||
Result:
|
||
|
||
- **Two schedules** are created when enabling New Activity:
|
||
- One **single** reminder (wrong for New Activity): static “Daily Check-In” message, same ID as Daily Reminder.
|
||
- One **dual** schedule (correct): prefetch + notify with API/fallback content.
|
||
- When the user turns **New Activity** off, AccountViewView only calls **`cancelDualSchedule()`**. It never calls `cancelDailyNotification()` (or equivalent) for the single reminder.
|
||
- So the **single** reminder stays scheduled and keeps firing at the chosen time. That’s the notification that “always fires” and “can’t be turned off.”
|
||
|
||
So the “huge problem with confusion with the web-push” is really: **the same dialog and the same “Turn on” path are used for both Daily Reminder and New Activity, but the dialog always schedules the single daily reminder on native**, while New Activity is supposed to use only the dual schedule. That mixing is what makes the wrong schedule stick and not be cancellable from the New Activity toggle.
|
||
|
||
---
|
||
|
||
## 3. Key Files and Flows
|
||
|
||
- **`src/components/PushNotificationPermission.vue`**
|
||
- Shared dialog for both “Daily Reminder” and “New Activity” (via `pushType` = `DIRECT_PUSH_TITLE` vs `DAILY_CHECK_TITLE`).
|
||
- On native it always uses `NotificationService.getInstance().scheduleDailyNotification(...)` (single reminder) and does not branch on “New Activity” to skip scheduling or to call the dual API.
|
||
- Saves `notifyingNewActivityTime` when `pushType === DAILY_CHECK_TITLE` (lines 834–836). So the dialog both schedules the wrong thing and persists settings for New Activity.
|
||
|
||
- **`src/views/AccountViewView.vue`**
|
||
- **Daily Reminder:** toggle opens same dialog with `DIRECT_PUSH_TITLE`; on native, disable path calls `service.cancelDailyNotification()`.
|
||
- **New Activity:** toggle opens same dialog with `DAILY_CHECK_TITLE`; on success callback calls `scheduleNewActivityDualNotification(timeText)`; on disable only calls `DailyNotification.cancelDualSchedule()`.
|
||
- `initializeState()`: on native with `activeDid`, calls `configureNativeFetcherIfReady(activeDid)` and, if New Activity is on, `updateStarredPlans(...)`. It does **not** re-call `scheduleNewActivityDualNotification` on load (so no double dual-schedule from here).
|
||
|
||
- **`src/services/notifications/NativeNotificationService.ts`**
|
||
- Single reminder only: `scheduleDailyNotification` → plugin `scheduleDailyNotification` with `id: this.reminderId` (`"daily_timesafari_reminder"`); `cancelDailyNotification` → `cancelDailyReminder({ reminderId })`. No dual API here.
|
||
|
||
- **`src/services/notifications/nativeFetcherConfig.ts`**
|
||
- Only configures the plugin for API calls (JWT, apiBaseUrl, activeDid). No scheduling.
|
||
|
||
- **`src/services/notifications/dualScheduleConfig.ts`**
|
||
- Builds config for `scheduleDualNotification` (contentFetch 5 min before, userNotification at notify time). Used only from AccountViewView’s `scheduleNewActivityDualNotification`.
|
||
|
||
- **`src/main.capacitor.ts`**
|
||
- Imports the daily-notification plugin; after a 2s delay calls `configureNativeFetcherIfReady()`. No scheduling; only fetcher config.
|
||
|
||
So: the “always fires / can’t turn off” behavior is from the **single** reminder created in `PushNotificationPermission` for New Activity and never cancelled when New Activity is turned off. The “confusion with web-push” is the reuse of the same dialog and the same native “schedule single reminder” path for both features.
|
||
|
||
---
|
||
|
||
## 4. Plugin Usage Summary
|
||
|
||
- **Single daily reminder (Daily Reminder):**
|
||
- Scheduled/cancelled via `NativeNotificationService.scheduleDailyNotification` / `cancelDailyNotification` → plugin `scheduleDailyNotification` / `cancelDailyReminder` with one `reminderId`.
|
||
- **Dual schedule (New Activity):**
|
||
- Scheduled/cancelled only in AccountViewView via `DailyNotification.scheduleDualNotification` / `cancelDualSchedule` (and `configureNativeFetcherIfReady` + `updateStarredPlans` as per doc).
|
||
- **Fetcher config (New Activity):**
|
||
- `configureNativeFetcherIfReady()` from main.capacitor and from AccountViewView `initializeState` / `scheduleNewActivityDualNotification`; no scheduling by itself.
|
||
|
||
---
|
||
|
||
## 5. Root Cause (Concise)
|
||
|
||
- **Single code path in PushNotificationPermission** for native: it always schedules the **single** daily reminder, regardless of `pushType` (Daily Reminder vs New Activity).
|
||
- For **New Activity**, that creates an extra, wrong schedule (single reminder) in addition to the correct dual schedule.
|
||
- **Disable path for New Activity** only calls `cancelDualSchedule()` and never cancels the single reminder, so that reminder keeps firing and appears as “always fires” and “can’t be turned off.”
|
||
|
||
---
|
||
|
||
## 6. Proper Fix: Options and Detail
|
||
|
||
A fix should ensure that (1) enabling New Activity creates only the dual schedule, and (2) disabling New Activity removes every schedule that was created for it. Below are concrete options and implementation notes.
|
||
|
||
### 6.1 Option A: Don’t schedule the single reminder when the dialog is for New Activity (recommended)
|
||
|
||
**Idea:** On native, when the dialog is opened for **New Activity** (`pushType === DAILY_CHECK_TITLE`), the dialog should **not** call `scheduleDailyNotification`. Only the callback in AccountViewView should run, and it already calls `scheduleNewActivityDualNotification(timeText)`, which uses the dual API only.
|
||
|
||
**Where:** `PushNotificationPermission.vue`, inside `turnOnNativeNotifications()`.
|
||
|
||
**Implementation sketch:**
|
||
|
||
- After requesting permissions and before calling `service.scheduleDailyNotification(...)`, branch on `pushType` and platform:
|
||
- If native **and** `pushType === this.DAILY_CHECK_TITLE`: skip the `scheduleDailyNotification` call entirely. Still run the rest of the flow (e.g. build `timeText`, save settings if desired, call `callback(true, timeText, ...)`). AccountViewView’s callback will then call `scheduleNewActivityDualNotification(timeText)` and that is the only schedule created for New Activity.
|
||
- Otherwise (web, or Daily Reminder on native): keep current behavior and call `scheduleDailyNotification` as today.
|
||
|
||
**Pros:** Single source of truth for “what is scheduled for New Activity” (dual only). No leftover single reminder to cancel later. Clear separation: dialog collects time + permission; AccountViewView owns native scheduling for New Activity.
|
||
|
||
**Cons:** Dialog’s native path now has two behaviors (schedule vs no schedule) depending on `pushType`; needs a quick comment so future changes don’t regress.
|
||
|
||
**Note:** The “edit reminder” flow already uses `skipSchedule: true` so the dialog doesn’t schedule; only the parent does. For New Activity enable, we’re doing the same idea: dialog doesn’t schedule on native, parent does.
|
||
|
||
### 6.2 Option B: When turning New Activity off, also cancel the single reminder
|
||
|
||
**Idea:** Assume the wrong single reminder might already exist (e.g. from before the fix, or from a different code path). When the user turns **New Activity** off, in addition to `cancelDualSchedule()`, call the service’s `cancelDailyNotification()` so the single reminder (same `reminderId` as Daily Reminder) is cancelled too.
|
||
|
||
**Where:** `AccountViewView.vue`, inside the disable branch of `showNewActivityNotificationChoice()` (where we currently only call `DailyNotification.cancelDualSchedule()`).
|
||
|
||
**Implementation sketch:**
|
||
|
||
- On native, when user confirms “turn off New Activity”:
|
||
1. Call `DailyNotification.cancelDualSchedule()` (existing).
|
||
2. Call `NotificationService.getInstance().cancelDailyNotification()` (new) so any single reminder that was mistakenly scheduled for this flow is removed.
|
||
|
||
**Pros:** Defensive: cleans up the bad schedule even if it was created in the past or by another path. Complements Option A (e.g. A prevents new wrong schedules; B cleans up existing ones).
|
||
|
||
**Cons:** That single `reminderId` is shared with **Daily Reminder**. If the user has **Daily Reminder** on and **New Activity** on, then turns only **New Activity** off, we must not cancel the reminder they still want for Daily Reminder. So either:
|
||
- Only call `cancelDailyNotification()` when we’re sure the single reminder was created for New Activity (e.g. we don’t have a separate “New Activity reminder ID”), which is hard without more state, or
|
||
- Don’t use Option B alone as the primary fix: use Option A so we never create the single reminder for New Activity, and only add B if we decide we need a one-time cleanup or a safety net (with care not to cancel Daily Reminder’s schedule).
|
||
|
||
**Recommendation:** Use Option A as the main fix. Add Option B only if the team agrees we need to cancel the single reminder on “New Activity off” and can do so without affecting Daily Reminder (e.g. by introducing a distinct reminder ID for a “New Activity legacy” reminder and only cancelling that, or by documenting that B is a one-time migration and not long-term behavior).
|
||
|
||
### 6.3 Optional cleanup: Separate reminder IDs or dialog responsibilities
|
||
|
||
- **Separate reminder IDs:** Today both Daily Reminder and the mistaken New Activity single reminder use `"daily_timesafari_reminder"`. If we ever want to support “both features on” and cancel only one, we’d need a second ID (e.g. one for Daily Reminder, one for New Activity). With Option A in place, New Activity no longer creates a single reminder, so we might not need a second ID unless we add a dedicated “New Activity fallback” single alarm later.
|
||
- **Dialog responsibilities:** We could narrow the dialog’s role when used for New Activity on native to “collect time + request permission and report success,” and leave all scheduling to AccountViewView. That’s what Option A does without necessarily refactoring the rest of the dialog (e.g. web push, Daily Reminder) in the same change.
|
||
- **Removing web-push logic for New Activity:** If the team decides to “totally remove” web-push logic that was added for New Activity, that would be a separate change (e.g. ensure New Activity on web either uses a different mechanism or is explicitly unsupported). The lay-of-the-land and this fix section focus on native; web can be scoped in a follow-up.
|
||
|
||
---
|
||
|
||
## 7. Testing New Activity on a Real Device (iOS or Android)
|
||
|
||
Use this section to verify the New Activity flow end-to-end on a physical device after implementing the fix (or to reproduce the current bug).
|
||
|
||
### Prerequisites
|
||
|
||
- **Build:** Native app built and installed (e.g. `npx cap sync` then build/run from Xcode or Android Studio), or a dev build on device.
|
||
- **Identity:** User is signed in (active DID set) so `configureNativeFetcherIfReady` and the native fetcher can use a valid JWT.
|
||
- **Endorser API URL:** New Activity prefetch uses **Account → API Server URL** (the Endorser base URL passed to `configureNativeFetcher`), not the Partner API URL. You can run these tests against **production, test, or local Endorser** (e.g. the test preset `https://test-api.endorser.ch`); use an identity, JWT, and starred plans that exist on **that** server. Changing only **Partner API** URL does not change where `plansLastUpdatedBetween` is called.
|
||
- **Optional:** One or more starred plans so the API can return activity; with zero starred plans the notification should still show with a sensible fallback (e.g. “No updates in your starred projects”).
|
||
|
||
### Enable flow
|
||
|
||
1. Open **Account** (Profile).
|
||
2. In the **Notifications** section, turn **New Activity Notification** on.
|
||
3. In the dialog, choose a time. For quick testing, set the device clock or pick a time **2–5 minutes from now** (e.g. if it’s 14:00, choose 14:03).
|
||
4. Tap **Turn on Daily Reminder** (or equivalent), grant notification permission when the OS prompts, and confirm the dialog closes and the toggle shows on with the chosen time.
|
||
5. **Background the app** (home or switch to another app). The prefetch runs ~5 minutes before the chosen time; the user notification fires at the chosen time.
|
||
|
||
### What to verify (after fix)
|
||
|
||
- **One notification** at the chosen time, with content from the API or the fallback text (e.g. “Check your starred projects and offers for updates.”). You should **not** see a second, static “Daily Check-In” / “Time to check your TimeSafari activity” notification from the old single-reminder path.
|
||
- **Before the fix:** You may see two notifications (one static from the mistaken single schedule, one from the dual schedule), and turning New Activity off will only stop the dual one; the static one will keep firing.
|
||
|
||
### Disable flow
|
||
|
||
1. On **Account**, turn **New Activity Notification** off and confirm in the “turn off” dialog.
|
||
2. Wait until the next occurrence of the previously chosen time (or use the same “time a few minutes ahead” trick and wait). **No notification** should appear. If one still appears, the single reminder was not cancelled (current bug or Option B not applied correctly).
|
||
|
||
### Device-specific notes
|
||
|
||
- **Android:** This app has **exact alarm disabled** (no `SCHEDULE_EXACT_ALARM`). Notification permission must be granted; delivery may be inexact or batched by the system. If the app is killed by the OS, behavior may depend on plugin boot/recovery behavior.
|
||
- **iOS:** Notification permission and background capabilities (e.g. background fetch) may affect prefetch. Test with app in background, not force-quit.
|
||
- **Time zone:** The chosen time is in the device’s local time. Ensure the device date/time and time zone are correct when testing.
|
||
|
||
### Optional test cases
|
||
|
||
- **No starred plans:** Enable New Activity with no starred projects; confirm no crash and a sensible fallback message in the notification.
|
||
- **JWT / API errors:** After leaving the app in background for a long time, the JWT may expire. Re-opening Account (or app) may re-run `configureNativeFetcherIfReady`; document or test whether a new notification still gets valid content or shows fallback.
|
||
- **Daily Reminder and New Activity both on:** With the fix, turning off only New Activity should not affect the Daily Reminder notification (they use different plugin APIs; Option B must not cancel the single reminder if the user still has Daily Reminder on).
|
||
|
||
### Testing: starred project with new activity (Android native fetcher)
|
||
|
||
Use this to verify that when a **starred** plan has **new** activity reported by `plansLastUpdatedBetween`, the notification shows API-derived copy (not only the dual-schedule default from `dualScheduleConfig.ts`).
|
||
|
||
The steps and expected notification copy below are **Android-specific**: this repo registers `TimeSafariNativeFetcher` only on Android today. Do not assume the same strings or behavior on iOS until native fetcher parity exists; see **`doc/notification-from-api-call.md`** (iOS checklist and remaining tasks).
|
||
|
||
**How it works (short):** On Android, `TimeSafariNativeFetcher` POSTs to `/api/v2/report/plansLastUpdatedBetween` with `planIds` from the plugin (`updateStarredPlans`) and `afterId` from stored `last_acked_jwt_id` (or `"0"` initially). When the response `data` array is **non-empty**, the fetcher builds **one** `NotificationContent`: title **Starred Project Update** (one row) or **Starred Project Updates** (two or more rows); body uses each row’s `plan.name` when present (else **Unnamed Project**). For a single update: `[name] has been updated.` For multiple: typographic quotes around the first row’s name, then ` + N more have been updated.` (with `N` = number of additional rows). When `data` is **empty**, the fetcher returns **no** notification items (no “nothing to report” notification). (See `android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java`.)
|
||
|
||
**Procedure (repeatable on device)**
|
||
|
||
1. Sign in on the Endorser environment you mean to test (e.g. test API URL in Account—see **Prerequisites**, Endorser API URL) so `configureNativeFetcherIfReady` can set JWT and `activeDid`.
|
||
2. Star at least one project you can change (e.g. your own test plan on staging).
|
||
3. Turn **New Activity Notification** on and pick a time **2–5 minutes ahead** (same quick-test pattern as above).
|
||
4. Open **Account** once (or finish the enable flow) so `updateStarredPlans({ planIds })` runs with current `starredPlanHandleIds`.
|
||
5. **Background the app** (home out; do not force-quit). Prefetch runs on the cron **~5 minutes before** the chosen time; the user notification fires at the chosen time.
|
||
6. **Produce new activity the API will return:** before that prefetch window (i.e. early enough that the scheduled content fetch still sees it), make a real change to the starred plan so `plansLastUpdatedBetween` returns **new** rows after the current `afterId` (e.g. an edit or other update your backend exposes through that report). If you change the plan **after** prefetch already ran with no new rows, you may not get an API-derived notification until the next prefetch cycle (typically the next day at the same T−5 schedule, unless you reschedule).
|
||
|
||
**What to verify**
|
||
|
||
- **One notification** at the chosen time (no extra static “Daily Check-In” after the fix—see “What to verify (after fix)” above).
|
||
- **Success path (API returns updates):** Title/body match **Starred Project Update(s)** and the `[name] has been updated.` / `[first name] + N more have been updated.` patterns (names from `plan.name`), not the generic `buildDualScheduleConfig` defaults (**New Activity** / **Check your starred projects and offers for updates.**), which apply when the plugin falls back—e.g. fetch failure—not when the Android fetcher successfully returns Endorser-parsed content.
|
||
- **Contrast (cursor caught up, no new rows):** After a successful fetch that returned data, `last_acked_jwt_id` advances. Without further plan changes, a later prefetch may return an empty `data` array; the fetcher then supplies **no** Endorser-derived notification (useful to compare against the “has activity” case; the plugin may still show dual-schedule fallback text depending on configuration).
|
||
|
||
**Repeatability:** Each successful fetch that returns data moves the `afterId` cursor forward. To see **Starred Project Update** copy again on subsequent tests, make **another** qualifying plan change (or accept heavier setup such as clearing app/plugin storage to reset cursor—usually unnecessary).
|
||
|
||
**Debugging:** On Android, filter **logcat** for `TimeSafariNativeFetcher` (e.g. HTTP 200, `Fetched N notification(s)`) to confirm prefetch ran and how many `NotificationContent` items were built.
|
||
|
||
**Note:** The in-app **New Activity** screen loads starred changes via the JS stack; the **push** path uses the native fetcher and plugin cache. Validate the notification using **background + prefetch timing**, not only by opening that screen.
|
||
|
||
---
|
||
|
||
## 8. Plugin Repo Alignment and Attention Items
|
||
|
||
Comparison with the **daily-notification-plugin** repo on gitea (`trent_larson/daily-notification-plugin`, `master` or the tag this app pins) to confirm our documentation and usage line up, and to flag anything that needs attention for the New Activity feature.
|
||
|
||
### 8.1 What lines up
|
||
|
||
- **API surface:** Plugin `definitions.ts` exposes `configureNativeFetcher({ apiBaseUrl, activeDid, jwtToken })`, `scheduleDualNotification(config)`, `cancelDualSchedule()`, `updateStarredPlans({ planIds })`, `scheduleDailyNotification(options)`, and `cancelDailyReminder(reminderId)`. Our app uses these as described in this doc; `buildDualScheduleConfig` produces a `DualScheduleConfiguration` that matches the plugin’s `ContentFetchConfig` / `UserNotificationConfig` / `relationship` shape (cron schedules, title/body, `callbacks: {}`, `fallbackBehavior: "show_default"`, etc.).
|
||
- **Native fetcher:** Plugin is designed for a host-supplied JWT via `configureNativeFetcher` and a native fetcher implementation (e.g. Android `TimeSafariNativeFetcher`). Our `nativeFetcherConfig.ts` and Android `TimeSafariNativeFetcher.java` follow that model; prefetch runs in the plugin’s background workers and uses the configured credentials.
|
||
- **Dual vs single:** The plugin clearly separates:
|
||
- **Single daily path:** `scheduleDailyNotification(options)` (with `id` on Android) and `cancelDailyReminder(reminderId)` (iOS uses `reminder_<reminderId>` for the static-reminder path).
|
||
- **Dual path:** `scheduleDualNotification(config)` and `cancelDualSchedule()`.
|
||
So our analysis that “two schedules” are created when the dialog schedules the single reminder and AccountViewView schedules the dual is consistent with the plugin.
|
||
- **Exact alarm:** The plugin’s Android implementation does **not** require exact alarm: it proceeds with scheduling using inexact/windowed alarms when exact is not granted. The plugin’s `INTEGRATION_GUIDE.md` still shows `SCHEDULE_EXACT_ALARM` in the manifest example; this app has chosen to disable exact alarm, and the plugin supports that. No doc change needed beyond what we already state in section 7.
|
||
|
||
### 8.2 Attention items
|
||
|
||
- **`cancelDailyReminder` signature:** In the plugin’s `definitions.ts`, `cancelDailyReminder(reminderId: string)`. The app calls it with an object: `cancelDailyReminder({ reminderId })`. On iOS the plugin uses `call.getString("reminderId")`, so the object form works. If the plugin’s TypeScript definition is ever used for strict typing, prefer updating the plugin to accept `{ reminderId: string }` or document that the bridge accepts an object with a `reminderId` key.
|
||
- **Plugin INTEGRATION_GUIDE vs this app:** The guide describes generic polling, dual scheduling, and optional `SCHEDULE_EXACT_ALARM`. This app uses the dual-schedule + native-fetcher path only (no generic polling), and does not use exact alarm. When onboarding or debugging, treat the guide as the full plugin feature set; our flow is the “legacy dual scheduling” + native fetcher part plus `updateStarredPlans` and `configureNativeFetcher`.
|
||
- **iOS `scheduleDailyNotification` and stable `id`:** On **Android**, the plugin uses `options.getString("id")` as the stable `scheduleId` for “one per day” semantics and cleanup. On **iOS**, the implementation in the repo was observed to build notification content with an internally generated id (e.g. `daily_<timestamp>`) and not obviously use the app-provided `id` from the call. If the app ever relies on a stable id on iOS for the single reminder (e.g. to cancel or replace only that reminder), it’s worth confirming in the plugin’s iOS code whether the call’s `id` is read and used; if not, consider requesting or contributing a change so iOS also uses the app-provided id for consistency with Android.
|
||
- **Dual schedule and content fetch:** The plugin’s dual schedule runs the content-fetch job on its cron and then the user notification at the configured time; our config uses a 5-minute gap and `relationship.contentTimeout` / `fallbackBehavior: "show_default"`. The native fetcher is invoked by the plugin’s background layer when the content-fetch schedule fires; we don’t rely on JS `callbacks` in the config (we pass `callbacks: {}`). That matches the “native fetcher does the work” design.
|
||
|
||
### 8.3 iOS `UNIMPLEMENTED` on `scheduleDualNotification` (other methods work)
|
||
|
||
If iOS logs `scheduleNewActivityDualNotification failed: {"code":"UNIMPLEMENTED"}` while `configureNativeFetcher` succeeds, Capacitor is often rejecting the call in **JavaScript** because `scheduleDualNotification` is missing from `window.Capacitor.PluginHeaders` for `DailyNotification` (stale **Pods / Xcode binary** after upgrading the plugin). **Not** usually a missing Swift handler if `node_modules` already lists the method in `pluginMethods`.
|
||
|
||
**Recovery:** `npx cap sync ios`, `cd ios/App && pod install`, Xcode **Clean Build Folder**, rebuild. See **`doc/plugin-feedback-ios-scheduleDualNotification.md`** (troubleshooting section).
|
||
|
||
### 8.4 Summary
|
||
|
||
The plugin repo aligns with how we use it for New Activity (dual schedule + native fetcher, no generic polling, exact alarm optional). The main follow-ups are: (1) clarify or align `cancelDailyReminder` argument shape in the plugin if needed for typing/tooling, and (2) confirm on iOS whether `scheduleDailyNotification` uses the app-provided `id` for stable single-reminder semantics.
|