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).
This commit is contained in:
Jose Olarte III
2026-03-19 19:26:59 +08:00
parent 3c262c9eeb
commit 1389a166fa
6 changed files with 320 additions and 791 deletions

View File

@@ -209,6 +209,12 @@ Comparison with the **daily-notification-plugin** repo (e.g. `daily-notification
- **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), its worth confirming in the plugins iOS code whether the calls `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 plugins 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 plugins background layer when the content-fetch schedule fires; we dont rely on JS `callbacks` in the config (we pass `callbacks: {}`). That matches the “native fetcher does the work” design.
### 8.3 Summary
### 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.

View File

@@ -6,6 +6,31 @@
---
## 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.swift`s `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).

View File

@@ -86,7 +86,7 @@ PODS:
- SQLCipher/common (4.9.0)
- SQLCipher/standard (4.9.0):
- SQLCipher/common
- TimesafariDailyNotificationPlugin (2.0.0):
- TimesafariDailyNotificationPlugin (2.1.1):
- Capacitor
- ZIPFoundation (0.9.19)
@@ -172,7 +172,7 @@ SPEC CHECKSUMS:
nanopb: 438bc412db1928dac798aa6fd75726007be04262
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
TimesafariDailyNotificationPlugin: 3c12e8c39fc27f689f56cf4e57230a8c28611fcc
TimesafariDailyNotificationPlugin: ab9860e6ab9db8019f64f3c08f115a0c4ffd32d9
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
PODFILE CHECKSUM: 6d92bfa46c6c2d31d19b8c0c38f56a8ae9fd222f

935
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -48,6 +48,32 @@ import { configureNativeFetcherIfReady } from "@/services/notifications";
logger.log("[Capacitor] 🚀 Starting initialization");
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
// Diagnostic: log DailyNotification methods from native PluginHeaders (helps debug UNIMPLEMENTED)
type CapacitorWindow = {
Capacitor?: {
PluginHeaders?: Array<{ name: string; methods?: Array<{ name: string }> }>;
};
};
const cap =
typeof window !== "undefined"
? (window as unknown as CapacitorWindow).Capacitor
: undefined;
if (cap?.PluginHeaders) {
const dn = cap.PluginHeaders.find((h) => h.name === "DailyNotification");
const methodNames = dn?.methods?.map((m) => m.name) ?? null;
logger.log(
"[Capacitor] DNP PluginHeaders methods:",
methodNames ?? "DailyNotification NOT IN HEADERS",
);
if (methodNames && !methodNames.includes("scheduleDualNotification")) {
logger.warn(
"[Capacitor] scheduleDualNotification missing from PluginHeaders native plugin may be stale; try clearing Xcode DerivedData and rebuilding",
);
}
} else {
logger.warn("[Capacitor] Capacitor.PluginHeaders not present");
}
const app = initializeApp();
// Initialize API error handling for unhandled promise rejections

View File

@@ -1128,9 +1128,16 @@ export default class AccountViewView extends Vue {
if (Capacitor.isNativePlatform() && this.activeDid) {
void configureNativeFetcherIfReady(this.activeDid);
if (this.notifyingNewActivity && DailyNotification?.updateStarredPlans) {
if (this.notifyingNewActivity) {
const planIds = settings?.starredPlanHandleIds ?? [];
void DailyNotification.updateStarredPlans({ planIds });
// Capacitor proxy always exposes a function per key; native may omit the method → UNIMPLEMENTED.
void DailyNotification.updateStarredPlans({ planIds }).catch(
(e: unknown) => {
if ((e as { code?: string })?.code !== "UNIMPLEMENTED") {
logger.warn("[AccountViewView] updateStarredPlans failed", e);
}
},
);
}
}
}
@@ -1272,25 +1279,82 @@ export default class AccountViewView extends Vue {
return;
}
try {
const time24h = this.parseTimeTo24Hour(notifyTime);
await configureNativeFetcherIfReady(this.activeDid);
const settings = await this.$accountSettings();
const planIds = settings?.starredPlanHandleIds ?? [];
if (DailyNotification?.updateStarredPlans) {
try {
await DailyNotification.updateStarredPlans({ planIds });
} catch (e: unknown) {
if ((e as { code?: string })?.code !== "UNIMPLEMENTED") {
throw e;
}
logger.debug(
"[AccountViewView] updateStarredPlans not on native plugin; continuing to scheduleDualNotification",
);
}
const config = buildDualScheduleConfig({ notifyTime: time24h });
// Diagnostic: log what Capacitor sees at call time (helps debug UNIMPLEMENTED)
const cap = (typeof window !== "undefined" &&
(
window as unknown as {
Capacitor?: {
PluginHeaders?: Array<{
name: string;
methods?: Array<{ name: string }>;
}>;
};
}
).Capacitor) as
| {
PluginHeaders?: Array<{
name: string;
methods?: Array<{ name: string }>;
}>;
}
| undefined;
const dnHeader = cap?.PluginHeaders?.find(
(h) => h.name === "DailyNotification",
);
const methodsAtCall = dnHeader?.methods?.map((m) => m.name) ?? null;
logger.warn(
"[AccountViewView] Before scheduleDualNotification, PluginHeaders methods:",
methodsAtCall ?? "DailyNotification not in headers",
);
if (
methodsAtCall &&
!methodsAtCall.includes("scheduleDualNotification")
) {
logger.warn(
"[AccountViewView] scheduleDualNotification missing from PluginHeaders at call time bridge may be stale for this run",
);
}
const config = buildDualScheduleConfig({ notifyTime });
await plugin.scheduleDualNotification!({ config });
} catch (error) {
logger.error(
"[AccountViewView] scheduleNewActivityDualNotification failed:",
error,
);
const code = (error as { code?: string })?.code;
const err = error as {
code?: string;
errorMessage?: string;
message?: string;
};
const code = err?.code;
const msg = err?.errorMessage ?? err?.message ?? "";
if (code === "UNIMPLEMENTED") {
this.notify.error(
"New Activity scheduling is not yet available on this device. Please update the app when support is added.",
TIMEOUTS.STANDARD,
);
} else if (
msg.includes("BGTaskSchedulerErrorDomain") ||
msg.includes("error 1")
) {
this.notify.error(
"New Activity scheduling needs a real device and Background App Refresh enabled. It does not work in Simulator.",
TIMEOUTS.STANDARD,
);
} else {
this.notify.error(
"Could not schedule New Activity notification. Please try again.",
@@ -1499,7 +1563,8 @@ export default class AccountViewView extends Vue {
/**
* Edit existing New Activity notification time.
* Opens the dialog with current time; on success reschedules dual notification.
* Opens the dialog with current time; on success updates dual schedule via
* updateDualScheduleConfig when available (plugin v2.1.0+), else scheduleDualNotification.
*/
async editNewActivityNotification(): Promise<void> {
const dialog = this.$refs
@@ -1510,7 +1575,41 @@ export default class AccountViewView extends Vue {
async (success: boolean, timeText: string) => {
if (!success) return;
if (Capacitor.isNativePlatform()) {
await this.scheduleNewActivityDualNotification(timeText);
const time24h = this.parseTimeTo24Hour(timeText);
const config = buildDualScheduleConfig({ notifyTime: time24h });
const plugin = DailyNotification as unknown as {
updateDualScheduleConfig?: (opts: {
config: unknown;
}) => Promise<void>;
scheduleDualNotification?: (opts: {
config: unknown;
}) => Promise<void>;
};
try {
if (plugin.updateDualScheduleConfig) {
await plugin.updateDualScheduleConfig({ config });
} else {
await this.scheduleNewActivityDualNotification(timeText);
}
} catch (e) {
logger.warn(
"[AccountViewView] updateDualScheduleConfig failed, falling back to scheduleDualNotification:",
e,
);
try {
await this.scheduleNewActivityDualNotification(timeText);
} catch (fallbackError) {
logger.error(
"[AccountViewView] editNewActivityNotification schedule failed:",
fallbackError,
);
this.notify.error(
"Could not update New Activity time. Please try again.",
TIMEOUTS.STANDARD,
);
return;
}
}
}
await this.$saveSettings({ notifyingNewActivityTime: timeText });
this.notifyingNewActivityTime = timeText;