forked from trent_larson/crowd-funder-for-time-pwa
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:
@@ -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), 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 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.
|
||||
|
||||
@@ -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 **Capacitor’s 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** plugin’s `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` entry’s `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`
|
||||
|
||||
Capacitor’s `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).
|
||||
|
||||
@@ -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
935
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user